Link improvements and fixes (#4501)

* Add ExternalLink
* Replace <a> with <Link>
This commit is contained in:
DingDongSoLong4
2024-02-06 01:21:19 +02:00
committed by GitHub
parent 1d0fa27c71
commit cf8efa9035
28 changed files with 292 additions and 325 deletions

View File

@@ -9,6 +9,7 @@ import { ModalComponent } from "src/components/Shared/Modal";
import { getStashboxBase } from "src/utils/stashbox";
import { FormattedMessage, useIntl } from "react-intl";
import { faPaperPlane } from "@fortawesome/free-solid-svg-icons";
import { ExternalLink } from "../Shared/ExternalLink";
interface IProps {
type: "scene" | "performer";
@@ -108,12 +109,12 @@ export const SubmitStashBoxDraft: React.FC<IProps> = ({
<FormattedMessage id="stashbox.submission_successful" />
</h6>
<div>
<a target="_blank" rel="noreferrer noopener" href={reviewUrl}>
<ExternalLink href={reviewUrl}>
<FormattedMessage
id="stashbox.go_review_draft"
values={{ endpoint_name: selectedBox?.name }}
/>
</a>
</ExternalLink>
</div>
</>
);

View File

@@ -38,6 +38,7 @@ import { DetailImage } from "src/components/Shared/DetailImage";
import { useRatingKeybinds } from "src/hooks/keybinds";
import { useLoadStickyHeader } from "src/hooks/detailsPanel";
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
import { ExternalLink } from "src/components/Shared/ExternalLink";
interface IProps {
movie: GQL.MovieDataFragment;
@@ -274,15 +275,13 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
const renderClickableIcons = () => (
<span className="name-icons">
{movie.url && (
<Button className="minimal icon-link" title={movie.url}>
<a
href={TextUtils.sanitiseURL(movie.url)}
className="link"
target="_blank"
rel="noopener noreferrer"
>
<Icon icon={faLink} />
</a>
<Button
as={ExternalLink}
href={TextUtils.sanitiseURL(movie.url)}
className="minimal link"
title={movie.url}
>
<Icon icon={faLink} />
</Button>
)}
</span>

View File

@@ -3,6 +3,7 @@ import { useIntl } from "react-intl";
import * as GQL from "src/core/generated-graphql";
import TextUtils from "src/utils/text";
import { DetailItem } from "src/components/Shared/DetailItem";
import { Link } from "react-router-dom";
interface IMovieDetailsPanel {
movie: GQL.MovieDataFragment;
@@ -34,9 +35,9 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({
id="studio"
value={
movie.studio?.id ? (
<a href={`/studios/${movie.studio?.id}`} target="_self">
<Link to={`/studios/${movie.studio?.id}`}>
{movie.studio?.name}
</a>
</Link>
) : (
""
)

View File

@@ -45,6 +45,7 @@ import { useRatingKeybinds } from "src/hooks/keybinds";
import { DetailImage } from "src/components/Shared/DetailImage";
import { useLoadStickyHeader } from "src/hooks/detailsPanel";
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
import { ExternalLink } from "src/components/Shared/ExternalLink";
interface IProps {
performer: GQL.PerformerDataFragment;
@@ -493,57 +494,50 @@ const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
<Icon icon={faHeart} />
</Button>
{performer.url && (
<Button className="minimal icon-link" title={performer.url}>
<a
href={TextUtils.sanitiseURL(performer.url)}
className="link"
target="_blank"
rel="noopener noreferrer"
>
<Icon icon={faLink} />
</a>
<Button
as={ExternalLink}
href={TextUtils.sanitiseURL(performer.url)}
className="minimal link"
title={performer.url}
>
<Icon icon={faLink} />
</Button>
)}
{(urls ?? []).map((url, index) => (
<Button key={index} className="minimal icon-link" title={url}>
<a
href={TextUtils.sanitiseURL(url)}
className={`detail-link ${index}`}
target="_blank"
rel="noopener noreferrer"
>
<Icon icon={faLink} />
</a>
<Button
key={index}
as={ExternalLink}
href={TextUtils.sanitiseURL(url)}
className={`minimal link detail-link detail-link-${index}`}
title={url}
>
<Icon icon={faLink} />
</Button>
))}
{performer.twitter && (
<Button className="minimal icon-link" title={performer.twitter}>
<a
href={TextUtils.sanitiseURL(
performer.twitter,
TextUtils.twitterURL
)}
className="twitter"
target="_blank"
rel="noopener noreferrer"
>
<Icon icon={faTwitter} />
</a>
<Button
as={ExternalLink}
href={TextUtils.sanitiseURL(
performer.twitter,
TextUtils.twitterURL
)}
className="minimal link twitter"
title={performer.twitter}
>
<Icon icon={faTwitter} />
</Button>
)}
{performer.instagram && (
<Button className="minimal icon-link" title={performer.instagram}>
<a
href={TextUtils.sanitiseURL(
performer.instagram,
TextUtils.instagramURL
)}
className="instagram"
target="_blank"
rel="noopener noreferrer"
>
<Icon icon={faInstagram} />
</a>
<Button
as={ExternalLink}
href={TextUtils.sanitiseURL(
performer.instagram,
TextUtils.instagramURL
)}
className="minimal link instagram"
title={performer.instagram}
>
<Icon icon={faInstagram} />
</Button>
)}
</span>

View File

@@ -25,6 +25,7 @@ import {
Option as SelectOption,
} from "../Shared/FilterSelect";
import { useCompare } from "src/hooks/state";
import { Link } from "react-router-dom";
export type SelectObject = {
id: string;
@@ -86,10 +87,9 @@ export const PerformerSelect: React.FC<
...optionProps,
children: (
<span className="react-select-image-option">
<a
href={`/performers/${object.id}`}
<Link
to={`/performers/${object.id}`}
target="_blank"
rel="noreferrer"
className="performer-select-image-link"
>
<img
@@ -97,7 +97,7 @@ export const PerformerSelect: React.FC<
src={object.image_path ?? ""}
loading="lazy"
/>
</a>
</Link>
<span>{name}</span>
{object.disambiguation && (
<span className="performer-disambiguation">{` (${object.disambiguation})`}</span>

View File

@@ -27,22 +27,9 @@
color: #ff7373;
}
.link {
color: rgb(191, 204, 214);
}
.instagram {
color: pink;
}
.icon-link {
padding: 0;
a {
display: inline-block;
padding: $btn-padding-y $btn-padding-x;
}
}
}
.rating-number .form-control {

View File

@@ -85,7 +85,7 @@ const FileInfoPanel: React.FC<IFileInfoPanelProps> = (
url={NavUtils.makeScenesPHashMatchUrl(phash?.value)}
target="_self"
truncate
trusted
internal
/>
<URLField
id="path"

View File

@@ -2,6 +2,7 @@ import React from "react";
import { Button } from "react-bootstrap";
import { useIntl } from "react-intl";
import { useLatestVersion } from "src/core/StashService";
import { ExternalLink } from "../Shared/ExternalLink";
import { ConstantSetting, SettingGroup } from "./Inputs";
import { SettingSection } from "./SettingSection";
@@ -115,13 +116,9 @@ export const SettingsAboutPanel: React.FC = () => {
{ id: "config.about.stash_home" },
{
url: (
<a
href="https://github.com/stashapp/stash"
rel="noopener noreferrer"
target="_blank"
>
<ExternalLink href="https://github.com/stashapp/stash">
GitHub
</a>
</ExternalLink>
),
}
)}
@@ -131,13 +128,9 @@ export const SettingsAboutPanel: React.FC = () => {
{ id: "config.about.stash_wiki" },
{
url: (
<a
href="https://docs.stashapp.cc"
rel="noopener noreferrer"
target="_blank"
>
<ExternalLink href="https://docs.stashapp.cc">
Documentation
</a>
</ExternalLink>
),
}
)}
@@ -147,13 +140,9 @@ export const SettingsAboutPanel: React.FC = () => {
{ id: "config.about.stash_discord" },
{
url: (
<a
href="https://discord.gg/2TsNFKt"
rel="noopener noreferrer"
target="_blank"
>
<ExternalLink href="https://discord.gg/2TsNFKt">
Discord
</a>
</ExternalLink>
),
}
)}
@@ -163,13 +152,9 @@ export const SettingsAboutPanel: React.FC = () => {
{ id: "config.about.stash_open_collective" },
{
url: (
<a
href="https://opencollective.com/stashapp"
rel="noopener noreferrer"
target="_blank"
>
<ExternalLink href="https://opencollective.com/stashapp">
Open Collective
</a>
</ExternalLink>
),
}
)}

View File

@@ -7,6 +7,7 @@ import { BooleanSetting, StringListSetting, StringSetting } from "./Inputs";
import { useSettings } from "./context";
import { useIntl } from "react-intl";
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { ExternalLink } from "../Shared/ExternalLink";
export const SettingsLibraryPanel: React.FC = () => {
const intl = useIntl();
@@ -76,13 +77,9 @@ export const SettingsLibraryPanel: React.FC = () => {
{intl.formatMessage({
id: "config.general.excluded_video_patterns_desc",
})}
<a
href="https://docs.stashapp.cc/beginner-guides/exclude-file-configuration"
rel="noopener noreferrer"
target="_blank"
>
<ExternalLink href="https://docs.stashapp.cc/beginner-guides/exclude-file-configuration">
<Icon icon={faQuestionCircle} />
</a>
</ExternalLink>
</span>
}
value={general.excludes ?? undefined}
@@ -98,13 +95,9 @@ export const SettingsLibraryPanel: React.FC = () => {
{intl.formatMessage({
id: "config.general.excluded_image_gallery_patterns_desc",
})}
<a
href="https://docs.stashapp.cc/beginner-guides/exclude-file-configuration"
rel="noopener noreferrer"
target="_blank"
>
<ExternalLink href="https://docs.stashapp.cc/beginner-guides/exclude-file-configuration">
<Icon icon={faQuestionCircle} />
</a>
</ExternalLink>
</span>
}
value={general.imageExcludes ?? undefined}

View File

@@ -26,6 +26,7 @@ import {
AvailablePluginPackages,
InstalledPluginPackages,
} from "./PluginPackageManager";
import { ExternalLink } from "../Shared/ExternalLink";
interface IPluginSettingProps {
pluginID: string;
@@ -97,15 +98,12 @@ export const SettingsPluginsPanel: React.FC = () => {
function renderLink(url?: string) {
if (url) {
return (
<Button className="minimal">
<a
href={TextUtils.sanitiseURL(url)}
className="link"
target="_blank"
rel="noopener noreferrer"
>
<Icon icon={faLink} />
</a>
<Button
as={ExternalLink}
href={TextUtils.sanitiseURL(url)}
className="minimal link"
>
<Icon icon={faLink} />
</Button>
);
}

View File

@@ -23,6 +23,7 @@ import {
AvailableScraperPackages,
InstalledScraperPackages,
} from "./ScraperPackageManager";
import { ExternalLink } from "../Shared/ExternalLink";
interface IURLList {
urls: string[];
@@ -42,16 +43,7 @@ const URLList: React.FC<IURLList> = ({ urls }) => {
const sanitised = TextUtils.sanitiseURL(url);
const siteURL = linkSite(sanitised!);
return (
<a
href={siteURL}
className="link"
target="_blank"
rel="noopener noreferrer"
>
{sanitised}
</a>
);
return <ExternalLink href={siteURL}>{sanitised}</ExternalLink>;
}
}

View File

@@ -67,7 +67,7 @@
min-width: 100px;
text-align: right;
button {
.btn {
margin: 0.25rem;
}
}

View File

@@ -5,6 +5,7 @@ import { useHistory } from "react-router-dom";
import * as GQL from "src/core/generated-graphql";
import { useSystemStatus, mutateMigrate } from "src/core/StashService";
import { migrationNotes } from "src/docs/en/MigrationNotes";
import { ExternalLink } from "../Shared/ExternalLink";
import { LoadingIndicator } from "../Shared/LoadingIndicator";
import { MarkdownPage } from "../Shared/MarkdownPage";
@@ -35,18 +36,12 @@ export const Migrate: React.FC = () => {
: "";
const discordLink = (
<a href="https://discord.gg/2TsNFKt" target="_blank" rel="noreferrer">
Discord
</a>
<ExternalLink href="https://discord.gg/2TsNFKt">Discord</ExternalLink>
);
const githubLink = (
<a
href="https://github.com/stashapp/stash/issues"
target="_blank"
rel="noreferrer"
>
<ExternalLink href="https://github.com/stashapp/stash/issues">
<FormattedMessage id="setup.github_repository" />
</a>
</ExternalLink>
);
useEffect(() => {

View File

@@ -27,6 +27,7 @@ import {
faQuestionCircle,
} from "@fortawesome/free-solid-svg-icons";
import { releaseNotes } from "src/docs/en/ReleaseNotes";
import { ExternalLink } from "../Shared/ExternalLink";
export const Setup: React.FC = () => {
const { configuration, loading: configLoading } =
@@ -103,18 +104,12 @@ export const Setup: React.FC = () => {
}, [configuration]);
const discordLink = (
<a href="https://discord.gg/2TsNFKt" target="_blank" rel="noreferrer">
Discord
</a>
<ExternalLink href="https://discord.gg/2TsNFKt">Discord</ExternalLink>
);
const githubLink = (
<a
href="https://github.com/stashapp/stash/issues"
target="_blank"
rel="noreferrer"
>
<ExternalLink href="https://github.com/stashapp/stash/issues">
<FormattedMessage id="setup.github_repository" />
</a>
</ExternalLink>
);
function onConfigLocationChosen(inWorkDir: boolean) {
@@ -825,14 +820,9 @@ export const Setup: React.FC = () => {
id="setup.success.open_collective"
values={{
open_collective_link: (
<a
href="https://opencollective.com/stashapp"
target="_blank"
rel="noreferrer"
>
{" "}
OpenCollective{" "}
</a>
<ExternalLink href="https://opencollective.com/stashapp">
Open Collective
</ExternalLink>
),
}}
/>

View File

@@ -0,0 +1,5 @@
type IExternalLinkProps = JSX.IntrinsicElements["a"];
export const ExternalLink: React.FC<IExternalLinkProps> = (props) => {
return <a target="_blank" rel="noopener noreferrer" {...props} />;
};

View File

@@ -2,6 +2,7 @@ import React, { useMemo } from "react";
import { StashId } from "src/core/generated-graphql";
import { ConfigurationContext } from "src/hooks/Config";
import { getStashboxBase } from "src/utils/stashbox";
import { ExternalLink } from "./ExternalLink";
export type LinkType = "performers" | "scenes" | "studios";
@@ -26,9 +27,7 @@ export const StashIDPill: React.FC<{
return (
<span className="stash-id-pill" data-endpoint={endpointName}>
<span>{endpointName}</span>
<a href={link} target="_blank" rel="noopener noreferrer">
{stash_id}
</a>
<ExternalLink href={link}>{stash_id}</ExternalLink>
</span>
);
};

View File

@@ -45,6 +45,7 @@ import { DetailImage } from "src/components/Shared/DetailImage";
import { useRatingKeybinds } from "src/hooks/keybinds";
import { useLoadStickyHeader } from "src/hooks/detailsPanel";
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
import { ExternalLink } from "src/components/Shared/ExternalLink";
interface IProps {
studio: GQL.StudioDataFragment;
@@ -285,15 +286,13 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
const renderClickableIcons = () => (
<span className="name-icons">
{studio.url && (
<Button className="minimal icon-link" title={studio.url}>
<a
href={TextUtils.sanitiseURL(studio.url)}
className="link"
target="_blank"
rel="noopener noreferrer"
>
<Icon icon={faLink} />
</a>
<Button
as={ExternalLink}
href={TextUtils.sanitiseURL(studio.url)}
className="minimal link"
title={studio.url}
>
<Icon icon={faLink} />
</Button>
)}
</span>

View File

@@ -2,6 +2,7 @@ import React from "react";
import * as GQL from "src/core/generated-graphql";
import { DetailItem } from "src/components/Shared/DetailItem";
import { StashIDPill } from "src/components/Shared/StashID";
import { Link } from "react-router-dom";
interface IStudioDetailsPanel {
studio: GQL.StudioDataFragment;
@@ -51,9 +52,9 @@ export const StudioDetailsPanel: React.FC<IStudioDetailsPanel> = ({
id="parent_studios"
value={
studio.parent_studio?.name ? (
<a href={`/studios/${studio.parent_studio?.id}`} target="_self">
<Link to={`/studios/${studio.parent_studio?.id}`}>
{studio.parent_studio.name}
</a>
</Link>
) : (
""
)

View File

@@ -18,6 +18,7 @@ import {
faExternalLinkAlt,
faTimes,
} from "@fortawesome/free-solid-svg-icons";
import { ExternalLink } from "../Shared/ExternalLink";
interface IPerformerModalProps {
performer: GQL.ScrapedScenePerformerDataFragment;
@@ -82,12 +83,14 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
[name]: !excluded[name],
});
const renderField = (
function maybeRenderField(
name: string,
text: string | null | undefined,
truncate: boolean = true
) =>
text && (
) {
if (!text) return;
return (
<div className="row no-gutters">
<div className="col-5 performer-create-modal-field" key={name}>
{!create && (
@@ -112,11 +115,72 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
)}
</div>
);
}
const base = endpoint?.match(/https?:\/\/.*?\//)?.[0];
const link = base
? `${base}performers/${performer.remote_site_id}`
: undefined;
function maybeRenderImage() {
if (!images.length) return;
return (
<div className="col-5 image-selection">
<div className="performer-image">
{!create && (
<Button
onClick={() => toggleField("image")}
variant="secondary"
className={cx(
"performer-image-exclude",
excluded.image ? "text-muted" : "text-success"
)}
>
<Icon icon={excluded.image ? faTimes : faCheck} />
</Button>
)}
<img
src={images[imageIndex]}
className={cx({ "d-none": imageState !== "loaded" })}
alt=""
onLoad={() => handleLoad(imageIndex)}
onError={handleError}
/>
{imageState === "loading" && (
<LoadingIndicator message="Loading image..." />
)}
{imageState === "error" && (
<div className="h-100 d-flex justify-content-center align-items-center">
<b>Error loading image.</b>
</div>
)}
</div>
<div className="d-flex mt-3">
<Button onClick={setPrev} disabled={images.length === 1}>
<Icon icon={faArrowLeft} />
</Button>
<h5 className="flex-grow-1">
Select performer image
<br />
{imageIndex + 1} of {images.length}
</h5>
<Button onClick={setNext} disabled={images.length === 1}>
<Icon icon={faArrowRight} />
</Button>
</div>
</div>
);
}
function maybeRenderStashBoxLink() {
const base = endpoint?.match(/https?:\/\/.*?\//)?.[0];
if (!base) return;
return (
<h6 className="mt-2">
<ExternalLink href={`${base}performers/${performer.remote_site_id}`}>
<FormattedMessage id="stashbox.source" />
<Icon icon={faExternalLinkAlt} className="ml-2" />
</ExternalLink>
</h6>
);
}
function onSaveClicked() {
if (!performer.name) {
@@ -201,89 +265,37 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
>
<div className="row">
<div className="col-7">
{renderField("name", performer.name)}
{renderField("disambiguation", performer.disambiguation)}
{renderField("aliases", performer.aliases)}
{renderField(
{maybeRenderField("name", performer.name)}
{maybeRenderField("disambiguation", performer.disambiguation)}
{maybeRenderField("aliases", performer.aliases)}
{maybeRenderField(
"gender",
performer.gender
? intl.formatMessage({ id: "gender_types." + performer.gender })
: ""
)}
{renderField("birthdate", performer.birthdate)}
{renderField("death_date", performer.death_date)}
{renderField("ethnicity", performer.ethnicity)}
{renderField("country", getCountryByISO(performer.country))}
{renderField("hair_color", performer.hair_color)}
{renderField("eye_color", performer.eye_color)}
{renderField("height", performer.height)}
{renderField("weight", performer.weight)}
{renderField("measurements", performer.measurements)}
{maybeRenderField("birthdate", performer.birthdate)}
{maybeRenderField("death_date", performer.death_date)}
{maybeRenderField("ethnicity", performer.ethnicity)}
{maybeRenderField("country", getCountryByISO(performer.country))}
{maybeRenderField("hair_color", performer.hair_color)}
{maybeRenderField("eye_color", performer.eye_color)}
{maybeRenderField("height", performer.height)}
{maybeRenderField("weight", performer.weight)}
{maybeRenderField("measurements", performer.measurements)}
{performer?.gender !== GQL.GenderEnum.Male &&
renderField("fake_tits", performer.fake_tits)}
{renderField("career_length", performer.career_length)}
{renderField("tattoos", performer.tattoos, false)}
{renderField("piercings", performer.piercings, false)}
{renderField("weight", performer.weight, false)}
{renderField("details", performer.details)}
{renderField("url", performer.url)}
{renderField("twitter", performer.twitter)}
{renderField("instagram", performer.instagram)}
{link && (
<h6 className="mt-2">
<a href={link} target="_blank" rel="noopener noreferrer">
<FormattedMessage id="stashbox.source" />
<Icon icon={faExternalLinkAlt} className="ml-2" />
</a>
</h6>
)}
maybeRenderField("fake_tits", performer.fake_tits)}
{maybeRenderField("career_length", performer.career_length)}
{maybeRenderField("tattoos", performer.tattoos, false)}
{maybeRenderField("piercings", performer.piercings, false)}
{maybeRenderField("weight", performer.weight, false)}
{maybeRenderField("details", performer.details)}
{maybeRenderField("url", performer.url)}
{maybeRenderField("twitter", performer.twitter)}
{maybeRenderField("instagram", performer.instagram)}
{maybeRenderStashBoxLink()}
</div>
{images.length > 0 && (
<div className="col-5 image-selection">
<div className="performer-image">
{!create && (
<Button
onClick={() => toggleField("image")}
variant="secondary"
className={cx(
"performer-image-exclude",
excluded.image ? "text-muted" : "text-success"
)}
>
<Icon icon={excluded.image ? faTimes : faCheck} />
</Button>
)}
<img
src={images[imageIndex]}
className={cx({ "d-none": imageState !== "loaded" })}
alt=""
onLoad={() => handleLoad(imageIndex)}
onError={handleError}
/>
{imageState === "loading" && (
<LoadingIndicator message="Loading image..." />
)}
{imageState === "error" && (
<div className="h-100 d-flex justify-content-center align-items-center">
<b>Error loading image.</b>
</div>
)}
</div>
<div className="d-flex mt-3">
<Button onClick={setPrev} disabled={images.length === 1}>
<Icon icon={faArrowLeft} />
</Button>
<h5 className="flex-grow-1">
Select performer image
<br />
{imageIndex + 1} of {images.length}
</h5>
<Button onClick={setNext} disabled={images.length === 1}>
<Icon icon={faArrowRight} />
</Button>
</div>
</div>
)}
{maybeRenderImage()}
</div>
</ModalComponent>
);

View File

@@ -26,6 +26,7 @@ import PerformerModal from "../PerformerModal";
import { useUpdatePerformer } from "../queries";
import { faStar, faTags } from "@fortawesome/free-solid-svg-icons";
import { mergeStashIDs } from "src/utils/stashbox";
import { ExternalLink } from "src/components/Shared/ExternalLink";
type JobFragment = Pick<
GQL.Job,
@@ -466,14 +467,12 @@ const PerformerTaggerList: React.FC<IPerformerTaggerListProps> = ({
if (stashID !== undefined) {
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
const link = base ? (
<a
<ExternalLink
className="small d-block"
href={`${base}performers/${stashID.stash_id}`}
target="_blank"
rel="noopener noreferrer"
>
{stashID.stash_id}
</a>
</ExternalLink>
) : (
<div className="small">{stashID.stash_id}</div>
);

View File

@@ -12,6 +12,7 @@ import {
PerformerSelect,
} from "src/components/Performers/PerformerSelect";
import { getStashboxBase } from "src/utils/stashbox";
import { ExternalLink } from "src/components/Shared/ExternalLink";
interface IPerformerName {
performer: GQL.ScrapedPerformer | Performer;
@@ -26,9 +27,7 @@ const PerformerName: React.FC<IPerformerName> = ({
}) => {
const name =
baseURL && id ? (
<a href={`${baseURL}${id}`} target="_blank" rel="noreferrer">
{performer.name}
</a>
<ExternalLink href={`${baseURL}${id}`}>{performer.name}</ExternalLink>
) : (
performer.name
);

View File

@@ -24,6 +24,7 @@ import StudioResult from "./StudioResult";
import { useInitialState } from "src/hooks/state";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { getStashboxBase } from "src/utils/stashbox";
import { ExternalLink } from "src/components/Shared/ExternalLink";
const getDurationStatus = (
scene: IScrapedScene,
@@ -488,14 +489,9 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
const url = scene.urls?.length ? scene.urls[0] : null;
const sceneTitleEl = url ? (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="scene-link"
>
<ExternalLink className="scene-link" href={url}>
<TruncatedText text={scene.title} />
</a>
</ExternalLink>
) : (
<TruncatedText text={scene.title} />
);
@@ -592,9 +588,7 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
>
{scene.urls.map((url) => (
<div key={url}>
<a href={url} target="_blank" rel="noopener noreferrer">
{url}
</a>
<ExternalLink href={url}>{url}</ExternalLink>
</div>
))}
</OptionalField>
@@ -626,9 +620,9 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
exclude={excludedFields[fields.stash_ids]}
setExclude={(v) => setExcludedField(fields.stash_ids, v)}
>
<a href={stashBoxURL} target="_blank" rel="noopener noreferrer">
<ExternalLink href={stashBoxURL}>
{scene.remote_site_id}
</a>
</ExternalLink>
</OptionalField>
</div>
);

View File

@@ -15,6 +15,7 @@ import {
import { Button, Form } from "react-bootstrap";
import { TruncatedText } from "src/components/Shared/TruncatedText";
import { excludeFields } from "src/utils/data";
import { ExternalLink } from "src/components/Shared/ExternalLink";
interface IStudioDetailsProps {
studio: GQL.ScrapedSceneStudioDataFragment;
@@ -83,15 +84,15 @@ const StudioDetails: React.FC<IStudioDetailsProps> = ({
);
}
function maybeRenderLink() {
function maybeRenderStashBoxLink() {
if (!link) return;
return (
<h6 className="mt-2">
<a href={link} target="_blank" rel="noopener noreferrer">
<ExternalLink href={link}>
<FormattedMessage id="stashbox.source" />
<Icon icon={faExternalLinkAlt} className="ml-2" />
</a>
</ExternalLink>
</h6>
);
}
@@ -104,7 +105,7 @@ const StudioDetails: React.FC<IStudioDetailsProps> = ({
{maybeRenderField("name", studio.name, !isNew)}
{maybeRenderField("url", studio.url)}
{maybeRenderField("parent_studio", studio.parent?.name, false)}
{maybeRenderLink()}
{maybeRenderStashBoxLink()}
</div>
</div>
</div>

View File

@@ -11,6 +11,7 @@ import * as GQL from "src/core/generated-graphql";
import { OptionalField } from "../IncludeButton";
import { faSave } from "@fortawesome/free-solid-svg-icons";
import { getStashboxBase } from "src/utils/stashbox";
import { ExternalLink } from "src/components/Shared/ExternalLink";
interface IStudioName {
studio: GQL.ScrapedStudio | GQL.SlimStudioDataFragment;
@@ -21,9 +22,7 @@ interface IStudioName {
const StudioName: React.FC<IStudioName> = ({ studio, id, baseURL }) => {
const name =
baseURL && id ? (
<a href={`${baseURL}${id}`} target="_blank" rel="noreferrer">
{studio.name}
</a>
<ExternalLink href={`${baseURL}${id}`}>{studio.name}</ExternalLink>
) : (
studio.name
);

View File

@@ -18,6 +18,7 @@ import {
faImage,
} from "@fortawesome/free-solid-svg-icons";
import { objectPath, objectTitle } from "src/core/files";
import { ExternalLink } from "src/components/Shared/ExternalLink";
interface ITaggerSceneDetails {
scene: GQL.SlimSceneDataFragment;
@@ -174,15 +175,13 @@ export const TaggerScene: React.FC<PropsWithChildren<ITaggerScene>> = ({
const stashLinks = scene.stash_ids.map((stashID) => {
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
const link = base ? (
<a
<ExternalLink
key={`${stashID.endpoint}${stashID.stash_id}`}
className="small d-block"
href={`${base}scenes/${stashID.stash_id}`}
target="_blank"
rel="noopener noreferrer"
>
{stashID.stash_id}
</a>
</ExternalLink>
) : (
<div className="small">{stashID.stash_id}</div>
);

View File

@@ -27,6 +27,7 @@ import StudioModal from "../scenes/StudioModal";
import { useUpdateStudio } from "../queries";
import { apolloError } from "src/utils";
import { faStar, faTags } from "@fortawesome/free-solid-svg-icons";
import { ExternalLink } from "src/components/Shared/ExternalLink";
type JobFragment = Pick<
GQL.Job,
@@ -515,14 +516,12 @@ const StudioTaggerList: React.FC<IStudioTaggerListProps> = ({
if (stashID !== undefined) {
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
const link = base ? (
<a
<ExternalLink
className="small d-block"
href={`${base}studios/${stashID.stash_id}`}
target="_blank"
rel="noopener noreferrer"
>
{stashID.stash_id}
</a>
</ExternalLink>
) : (
<div className="small">{stashID.stash_id}</div>
);

View File

@@ -205,6 +205,15 @@ dd {
}
}
.btn.link {
color: $link-color;
&:hover:not(:disabled),
&:active:not(:disabled) {
color: $link-color;
}
}
.detail-header.edit {
background-color: unset;
overflow: visible;
@@ -691,12 +700,11 @@ div.dropdown-menu {
flex-wrap: wrap;
row-gap: 10px;
/* stylelint-disable */
.badge {
white-space: normal !important;
margin: unset;
// stylelint-disable declaration-no-important
white-space: normal !important;
}
/* stylelint-enable */
}
.tag-item {

View File

@@ -1,5 +1,7 @@
import React from "react";
import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom";
import { ExternalLink } from "src/components/Shared/ExternalLink";
import { TruncatedText } from "src/components/Shared/TruncatedText";
interface ITextField {
@@ -44,8 +46,8 @@ interface IURLField {
url?: string | null;
truncate?: boolean | null;
target?: string;
// use for internal links
trusted?: boolean;
// an internal link (uses <Link to={url}>)
internal?: boolean;
}
export const URLField: React.FC<IURLField> = ({
@@ -55,11 +57,10 @@ export const URLField: React.FC<IURLField> = ({
url,
abbr,
truncate,
children,
target,
trusted,
target = "_blank",
internal,
}) => {
if (!value && !children) {
if (!value) {
return null;
}
@@ -67,26 +68,30 @@ export const URLField: React.FC<IURLField> = ({
<>{id ? <FormattedMessage id={id} defaultMessage={name} /> : name}:</>
);
const rel = !trusted ? "noopener noreferrer" : undefined;
function maybeRenderUrl() {
if (!url) return;
const children = truncate ? <TruncatedText text={value} /> : value;
if (internal) {
return (
<Link to={url} target={target}>
{children}
</Link>
);
} else {
return (
<ExternalLink href={url} target={target}>
{children}
</ExternalLink>
);
}
}
return (
<>
<dt>{abbr ? <abbr title={abbr}>{message}</abbr> : message}</dt>
<dd>
{url ? (
<a href={url} target={target || "_blank"} rel={rel}>
{value ? (
truncate ? (
<TruncatedText text={value} />
) : (
value
)
) : (
children
)}
</a>
) : undefined}
</dd>
<dd>{maybeRenderUrl()}</dd>
</>
);
};
@@ -98,8 +103,8 @@ interface IURLsField {
urls?: string[] | null;
truncate?: boolean | null;
target?: string;
// use for internal links
trusted?: boolean;
// an internal link (uses <Link to={url}>)
internal?: boolean;
}
export const URLsField: React.FC<IURLsField> = ({
@@ -108,11 +113,10 @@ export const URLsField: React.FC<IURLsField> = ({
urls,
abbr,
truncate,
target,
trusted,
target = "_blank",
internal,
}) => {
const values = urls ?? [];
if (!values.length) {
if (!urls || !urls.length) {
return null;
}
@@ -120,19 +124,33 @@ export const URLsField: React.FC<IURLsField> = ({
<>{id ? <FormattedMessage id={id} defaultMessage={name} /> : name}:</>
);
const rel = !trusted ? "noopener noreferrer" : undefined;
const renderUrls = () => {
return urls.map((url, i) => {
if (!url) return;
const children = truncate ? <TruncatedText text={url} /> : url;
if (internal) {
return (
<Link key={i} to={url} target={target}>
{children}
</Link>
);
} else {
return (
<ExternalLink key={i} href={url} target={target}>
{children}
</ExternalLink>
);
}
});
};
return (
<>
<dt>{abbr ? <abbr title={abbr}>{message}</abbr> : message}</dt>
<dd>
<dl>
{values.map((url, i) => (
<a key={i} href={url} target={target || "_blank"} rel={rel}>
{truncate ? <TruncatedText text={url} /> : url}
</a>
))}
</dl>
<dl>{renderUrls()}</dl>
</dd>
</>
);