Add page titles using react-helmet (#1831)

* add titles with react-helmet
This commit is contained in:
stashcoder42
2021-10-26 18:37:18 -04:00
committed by GitHub
parent e9c7b0aed3
commit bdb8dc94d3
21 changed files with 349 additions and 140 deletions

View File

@@ -58,6 +58,7 @@
"react": "17.0.1", "react": "17.0.1",
"react-bootstrap": "1.4.3", "react-bootstrap": "1.4.3",
"react-dom": "17.0.1", "react-dom": "17.0.1",
"react-helmet": "^6.1.0",
"react-intl": "^5.10.16", "react-intl": "^5.10.16",
"react-jw-player": "1.19.1", "react-jw-player": "1.19.1",
"react-markdown": "^5.0.3", "react-markdown": "^5.0.3",
@@ -89,6 +90,7 @@
"@types/node": "14.14.22", "@types/node": "14.14.22",
"@types/react": "17.0.0", "@types/react": "17.0.0",
"@types/react-dom": "^17.0.0", "@types/react-dom": "^17.0.0",
"@types/react-helmet": "^6.1.3",
"@types/react-router-bootstrap": "^0.24.5", "@types/react-router-bootstrap": "^0.24.5",
"@types/react-router-dom": "5.1.7", "@types/react-router-dom": "5.1.7",
"@types/react-router-hash-link": "^1.2.1", "@types/react-router-hash-link": "^1.2.1",

View File

@@ -1,6 +1,7 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { Route, Switch, useRouteMatch } from "react-router-dom"; import { Route, Switch, useRouteMatch } from "react-router-dom";
import { IntlProvider } from "react-intl"; import { IntlProvider } from "react-intl";
import { Helmet } from "react-helmet";
import { mergeWith } from "lodash"; import { mergeWith } from "lodash";
import { ToastProvider } from "src/hooks/Toast"; import { ToastProvider } from "src/hooks/Toast";
import LightboxProvider from "src/hooks/Lightbox/context"; import LightboxProvider from "src/hooks/Lightbox/context";
@@ -30,7 +31,7 @@ import Images from "./components/Images/Images";
import { Setup } from "./components/Setup/Setup"; import { Setup } from "./components/Setup/Setup";
import { Migrate } from "./components/Setup/Migrate"; import { Migrate } from "./components/Setup/Migrate";
import * as GQL from "./core/generated-graphql"; import * as GQL from "./core/generated-graphql";
import { LoadingIndicator } from "./components/Shared"; import { LoadingIndicator, TITLE_SUFFIX } from "./components/Shared";
import ConfigurationProvider from "./hooks/Config"; import ConfigurationProvider from "./hooks/Config";
initPolyfills(); initPolyfills();
@@ -145,6 +146,10 @@ export const App: React.FC = () => {
> >
<ToastProvider> <ToastProvider>
<LightboxProvider> <LightboxProvider>
<Helmet
titleTemplate={`%s ${TITLE_SUFFIX}`}
defaultTitle="Stash"
/>
{maybeRenderNavbar()} {maybeRenderNavbar()}
<div className="main container-fluid">{renderContent()}</div> <div className="main container-fluid">{renderContent()}</div>
</LightboxProvider> </LightboxProvider>

View File

@@ -5,6 +5,7 @@
* Added interface options to disable creating performers/studios/tags from dropdown selectors. ([#1814](https://github.com/stashapp/stash/pull/1814)) * Added interface options to disable creating performers/studios/tags from dropdown selectors. ([#1814](https://github.com/stashapp/stash/pull/1814))
### 🎨 Improvements ### 🎨 Improvements
* Added specific page titles. ([#1831](https://github.com/stashapp/stash/pull/1831))
* Added es-ES language option. ([#1886](https://github.com/stashapp/stash/pull/1886)) * Added es-ES language option. ([#1886](https://github.com/stashapp/stash/pull/1886))
* Show pagination at top and bottom of page. ([#1776](https://github.com/stashapp/stash/pull/1776)) * Show pagination at top and bottom of page. ([#1776](https://github.com/stashapp/stash/pull/1776))
* Include total duration/megapixels and filesize information on Scenes and Images pages. ([#1776](https://github.com/stashapp/stash/pull/1776)) * Include total duration/megapixels and filesize information on Scenes and Images pages. ([#1776](https://github.com/stashapp/stash/pull/1776))

View File

@@ -1,22 +1,38 @@
import React from "react"; import React from "react";
import { Route, Switch } from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import { useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import { TITLE_SUFFIX } from "src/components/Shared";
import { PersistanceLevel } from "src/hooks/ListHook"; import { PersistanceLevel } from "src/hooks/ListHook";
import Gallery from "./GalleryDetails/Gallery"; import Gallery from "./GalleryDetails/Gallery";
import GalleryCreate from "./GalleryDetails/GalleryCreate"; import GalleryCreate from "./GalleryDetails/GalleryCreate";
import { GalleryList } from "./GalleryList"; import { GalleryList } from "./GalleryList";
const Galleries = () => ( const Galleries = () => {
<Switch> const intl = useIntl();
<Route
exact const title_template = `${intl.formatMessage({
path="/galleries" id: "galleries",
render={(props) => ( })} ${TITLE_SUFFIX}`;
<GalleryList {...props} persistState={PersistanceLevel.ALL} /> return (
)} <>
/> <Helmet
<Route exact path="/galleries/new" component={GalleryCreate} /> defaultTitle={title_template}
<Route path="/galleries/:id/:tab?" component={Gallery} /> titleTemplate={`%s | ${title_template}`}
</Switch> />
); <Switch>
<Route
exact
path="/galleries"
render={(props) => (
<GalleryList {...props} persistState={PersistanceLevel.ALL} />
)}
/>
<Route exact path="/galleries/new" component={GalleryCreate} />
<Route path="/galleries/:id/:tab?" component={Gallery} />
</Switch>
</>
);
};
export default Galleries; export default Galleries;

View File

@@ -2,6 +2,7 @@ import { Tab, Nav, Dropdown } from "react-bootstrap";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useParams, useHistory, Link } from "react-router-dom"; import { useParams, useHistory, Link } from "react-router-dom";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { import {
mutateMetadataScan, mutateMetadataScan,
@@ -270,6 +271,11 @@ export const GalleryPage: React.FC<IProps> = ({ gallery }) => {
return ( return (
<div className="row"> <div className="row">
<Helmet>
<title>
{gallery.title ?? TextUtils.fileNameFromPath(gallery.path ?? "")}
</title>
</Helmet>
{maybeRenderDeleteDialog()} {maybeRenderDeleteDialog()}
<div className="gallery-tabs"> <div className="gallery-tabs">
<div className="d-none d-xl-block"> <div className="d-none d-xl-block">

View File

@@ -2,6 +2,7 @@ import { Tab, Nav, Dropdown } from "react-bootstrap";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { useParams, useHistory, Link } from "react-router-dom"; import { useParams, useHistory, Link } from "react-router-dom";
import { Helmet } from "react-helmet";
import { import {
useFindImage, useFindImage,
useImageIncrementO, useImageIncrementO,
@@ -262,6 +263,10 @@ export const Image: React.FC = () => {
return ( return (
<div className="row"> <div className="row">
<Helmet>
<title>{image.title ?? TextUtils.fileNameFromPath(image.path)}</title>
</Helmet>
{maybeRenderDeleteDialog()} {maybeRenderDeleteDialog()}
<div className="image-tabs order-xl-first order-last"> <div className="image-tabs order-xl-first order-last">
<div className="d-none d-xl-block"> <div className="d-none d-xl-block">

View File

@@ -1,20 +1,36 @@
import React from "react"; import React from "react";
import { Route, Switch } from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import { useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import { TITLE_SUFFIX } from "src/components/Shared";
import { PersistanceLevel } from "src/hooks/ListHook"; import { PersistanceLevel } from "src/hooks/ListHook";
import { Image } from "./ImageDetails/Image"; import { Image } from "./ImageDetails/Image";
import { ImageList } from "./ImageList"; import { ImageList } from "./ImageList";
const Images = () => ( const Images: React.FC = () => {
<Switch> const intl = useIntl();
<Route
exact const title_template = `${intl.formatMessage({
path="/images" id: "images",
render={(props) => ( })} ${TITLE_SUFFIX}`;
<ImageList persistState={PersistanceLevel.ALL} {...props} /> return (
)} <>
/> <Helmet
<Route path="/images/:id" component={Image} /> defaultTitle={title_template}
</Switch> titleTemplate={`%s | ${title_template}`}
); />
<Switch>
<Route
exact
path="/images"
render={(props) => (
<ImageList persistState={PersistanceLevel.ALL} {...props} />
)}
/>
<Route path="/images/:id" component={Image} />
</Switch>
</>
);
};
export default Images; export default Images;

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { import {
@@ -174,6 +175,10 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
// TODO: CSS class // TODO: CSS class
return ( return (
<div className="row"> <div className="row">
<Helmet>
<title>{movie?.name}</title>
</Helmet>
<div className="movie-details mb-3 col col-xl-4 col-lg-6"> <div className="movie-details mb-3 col col-xl-4 col-lg-6">
<div className="logo w-100"> <div className="logo w-100">
{encodingImage ? ( {encodingImage ? (

View File

@@ -1,15 +1,31 @@
import React from "react"; import React from "react";
import { Route, Switch } from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import { useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import { TITLE_SUFFIX } from "src/components/Shared";
import Movie from "./MovieDetails/Movie"; import Movie from "./MovieDetails/Movie";
import MovieCreate from "./MovieDetails/MovieCreate"; import MovieCreate from "./MovieDetails/MovieCreate";
import { MovieList } from "./MovieList"; import { MovieList } from "./MovieList";
const Movies = () => ( const Movies: React.FC = () => {
<Switch> const intl = useIntl();
<Route exact path="/movies" component={MovieList} />
<Route exact path="/movies/new" component={MovieCreate} /> const title_template = `${intl.formatMessage({
<Route path="/movies/:id/:tab?" component={Movie} /> id: "movies",
</Switch> })} ${TITLE_SUFFIX}`;
); return (
<>
<Helmet
defaultTitle={title_template}
titleTemplate={`%s | ${title_template}`}
/>
<Switch>
<Route exact path="/movies" component={MovieList} />
<Route exact path="/movies/new" component={MovieCreate} />
<Route path="/movies/:id/:tab?" component={Movie} />
</Switch>
</>
);
};
export default Movies; export default Movies;

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from "react";
import { Button, Tabs, Tab } from "react-bootstrap"; import { Button, Tabs, Tab } from "react-bootstrap";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { useParams, useHistory } from "react-router-dom"; import { useParams, useHistory } from "react-router-dom";
import { Helmet } from "react-helmet";
import cx from "classnames"; import cx from "classnames";
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
@@ -299,6 +300,10 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
return ( return (
<div id="performer-page" className="row"> <div id="performer-page" className="row">
<Helmet>
<title>{performer.name}</title>
</Helmet>
<div className="performer-image-container col-md-4 text-center"> <div className="performer-image-container col-md-4 text-center">
{imageEncoding ? ( {imageEncoding ? (
<LoadingIndicator message="Encoding image..." /> <LoadingIndicator message="Encoding image..." />

View File

@@ -1,22 +1,37 @@
import React from "react"; import React from "react";
import { Route, Switch } from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import { useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import { TITLE_SUFFIX } from "src/components/Shared";
import { PersistanceLevel } from "src/hooks/ListHook"; import { PersistanceLevel } from "src/hooks/ListHook";
import Performer from "./PerformerDetails/Performer"; import Performer from "./PerformerDetails/Performer";
import PerformerCreate from "./PerformerDetails/PerformerCreate"; import PerformerCreate from "./PerformerDetails/PerformerCreate";
import { PerformerList } from "./PerformerList"; import { PerformerList } from "./PerformerList";
const Performers = () => ( const Performers: React.FC = () => {
<Switch> const intl = useIntl();
<Route
exact
path="/performers"
render={(props) => (
<PerformerList persistState={PersistanceLevel.ALL} {...props} />
)}
/>
<Route path="/performers/new" component={PerformerCreate} />
<Route path="/performers/:id/:tab?" component={Performer} />
</Switch>
);
const title_template = `${intl.formatMessage({
id: "performers",
})} ${TITLE_SUFFIX}`;
return (
<>
<Helmet
defaultTitle={title_template}
titleTemplate={`%s | ${title_template}`}
/>
<Switch>
<Route
exact
path="/performers"
render={(props) => (
<PerformerList persistState={PersistanceLevel.ALL} {...props} />
)}
/>
<Route path="/performers/new" component={PerformerCreate} />
<Route path="/performers/:id/:tab?" component={Performer} />
</Switch>
</>
);
};
export default Performers; export default Performers;

View File

@@ -3,6 +3,7 @@ import queryString from "query-string";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { useParams, useLocation, useHistory, Link } from "react-router-dom"; import { useParams, useLocation, useHistory, Link } from "react-router-dom";
import { Helmet } from "react-helmet";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { import {
mutateMetadataScan, mutateMetadataScan,
@@ -566,6 +567,9 @@ const ScenePage: React.FC<IProps> = ({ scene, refetch }) => {
return ( return (
<div className="row"> <div className="row">
<Helmet>
<title>{scene.title ?? TextUtils.fileNameFromPath(scene.path)}</title>
</Helmet>
{maybeRenderSceneGenerateDialog()} {maybeRenderSceneGenerateDialog()}
{maybeRenderDeleteDialog()} {maybeRenderDeleteDialog()}
<div <div

View File

@@ -2,6 +2,8 @@ import _ from "lodash";
import React from "react"; import React from "react";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import { TITLE_SUFFIX } from "src/components/Shared";
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
import { FindSceneMarkersQueryResult } from "src/core/generated-graphql"; import { FindSceneMarkersQueryResult } from "src/core/generated-graphql";
import { queryFindSceneMarkers } from "src/core/StashService"; import { queryFindSceneMarkers } from "src/core/StashService";
@@ -81,6 +83,18 @@ export const SceneMarkerList: React.FC<ISceneMarkerList> = ({ filterHook }) => {
); );
} }
} }
const title_template = `${intl.formatMessage({
id: "markers",
})} ${TITLE_SUFFIX}`;
return listData.template; return (
<>
<Helmet
defaultTitle={title_template}
titleTemplate={`%s | ${title_template}`}
/>
{listData.template}
</>
);
}; };

View File

@@ -1,22 +1,37 @@
import React from "react"; import React from "react";
import { Route, Switch } from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import { useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import { TITLE_SUFFIX } from "src/components/Shared";
import { PersistanceLevel } from "src/hooks/ListHook"; import { PersistanceLevel } from "src/hooks/ListHook";
import Scene from "./SceneDetails/Scene"; import Scene from "./SceneDetails/Scene";
import { SceneList } from "./SceneList"; import { SceneList } from "./SceneList";
import { SceneMarkerList } from "./SceneMarkerList"; import { SceneMarkerList } from "./SceneMarkerList";
const Scenes = () => ( const Scenes: React.FC = () => {
<Switch> const intl = useIntl();
<Route
exact
path="/scenes"
render={(props) => (
<SceneList persistState={PersistanceLevel.ALL} {...props} />
)}
/>
<Route exact path="/scenes/markers" component={SceneMarkerList} />
<Route path="/scenes/:id" component={Scene} />
</Switch>
);
const title_template = `${intl.formatMessage({
id: "scenes",
})} ${TITLE_SUFFIX}`;
return (
<>
<Helmet
defaultTitle={title_template}
titleTemplate={`%s | ${title_template}`}
/>
<Switch>
<Route
exact
path="/scenes"
render={(props) => (
<SceneList persistState={PersistanceLevel.ALL} {...props} />
)}
/>
<Route exact path="/scenes/markers" component={SceneMarkerList} />
<Route path="/scenes/:id" component={Scene} />
</Switch>
</>
);
};
export default Scenes; export default Scenes;

View File

@@ -2,7 +2,9 @@ import React from "react";
import queryString from "query-string"; import queryString from "query-string";
import { Card, Tab, Nav, Row, Col } from "react-bootstrap"; import { Card, Tab, Nav, Row, Col } from "react-bootstrap";
import { useHistory, useLocation } from "react-router-dom"; import { useHistory, useLocation } from "react-router-dom";
import { FormattedMessage } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import { TITLE_SUFFIX } from "src/components/Shared";
import { SettingsAboutPanel } from "./SettingsAboutPanel"; import { SettingsAboutPanel } from "./SettingsAboutPanel";
import { SettingsConfigurationPanel } from "./SettingsConfigurationPanel"; import { SettingsConfigurationPanel } from "./SettingsConfigurationPanel";
import { SettingsInterfacePanel } from "./SettingsInterfacePanel/SettingsInterfacePanel"; import { SettingsInterfacePanel } from "./SettingsInterfacePanel/SettingsInterfacePanel";
@@ -14,14 +16,22 @@ import { SettingsToolsPanel } from "./SettingsToolsPanel";
import { SettingsDLNAPanel } from "./SettingsDLNAPanel"; import { SettingsDLNAPanel } from "./SettingsDLNAPanel";
export const Settings: React.FC = () => { export const Settings: React.FC = () => {
const intl = useIntl();
const location = useLocation(); const location = useLocation();
const history = useHistory(); const history = useHistory();
const defaultTab = queryString.parse(location.search).tab ?? "tasks"; const defaultTab = queryString.parse(location.search).tab ?? "tasks";
const onSelect = (val: string) => history.push(`?tab=${val}`); const onSelect = (val: string) => history.push(`?tab=${val}`);
const title_template = `${intl.formatMessage({
id: "settings",
})} ${TITLE_SUFFIX}`;
return ( return (
<Card className="col col-lg-9 mx-auto"> <Card className="col col-lg-9 mx-auto">
<Helmet
defaultTitle={title_template}
titleTemplate={`%s | ${title_template}`}
/>
<Tab.Container <Tab.Container
activeKey={defaultTab} activeKey={defaultTab}
id="configuration-tabs" id="configuration-tabs"

View File

@@ -18,3 +18,4 @@ export { RatingStars } from "./RatingStars";
export { ExportDialog } from "./ExportDialog"; export { ExportDialog } from "./ExportDialog";
export { default as DeleteEntityDialog } from "./DeleteEntityDialog"; export { default as DeleteEntityDialog } from "./DeleteEntityDialog";
export { OperationButton } from "./OperationButton"; export { OperationButton } from "./OperationButton";
export const TITLE_SUFFIX = " | Stash";

View File

@@ -2,6 +2,7 @@ import { Tabs, Tab } from "react-bootstrap";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useParams, useHistory } from "react-router-dom"; import { useParams, useHistory } from "react-router-dom";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
@@ -179,6 +180,11 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
</div> </div>
{!isEditing ? ( {!isEditing ? (
<> <>
<Helmet>
<title>
{studio.name ?? intl.formatMessage({ id: "studio" })}
</title>
</Helmet>
<StudioDetailsPanel studio={studio} /> <StudioDetailsPanel studio={studio} />
<DetailsEditNavbar <DetailsEditNavbar
objectName={studio.name ?? intl.formatMessage({ id: "studio" })} objectName={studio.name ?? intl.formatMessage({ id: "studio" })}

View File

@@ -1,15 +1,30 @@
import React from "react"; import React from "react";
import { Route, Switch } from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import { useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import { TITLE_SUFFIX } from "src/components/Shared";
import Studio from "./StudioDetails/Studio"; import Studio from "./StudioDetails/Studio";
import StudioCreate from "./StudioDetails/StudioCreate"; import StudioCreate from "./StudioDetails/StudioCreate";
import { StudioList } from "./StudioList"; import { StudioList } from "./StudioList";
const Studios = () => ( const Studios: React.FC = () => {
<Switch> const intl = useIntl();
<Route exact path="/studios" component={StudioList} />
<Route exact path="/studios/new" component={StudioCreate} />
<Route path="/studios/:id/:tab?" component={Studio} />
</Switch>
);
const title_template = `${intl.formatMessage({
id: "studios",
})} ${TITLE_SUFFIX}`;
return (
<>
<Helmet
defaultTitle={title_template}
titleTemplate={`%s | ${title_template}`}
/>
<Switch>
<Route exact path="/studios" component={StudioList} />
<Route exact path="/studios/new" component={StudioCreate} />
<Route path="/studios/:id/:tab?" component={Studio} />
</Switch>
</>
);
};
export default Studios; export default Studios;

View File

@@ -2,6 +2,7 @@ import { Tabs, Tab, Dropdown } from "react-bootstrap";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useParams, useHistory } from "react-router-dom"; import { useParams, useHistory } from "react-router-dom";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
@@ -248,76 +249,84 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
} }
return ( return (
<div className="row"> <>
<div className="tag-details col-md-4"> <Helmet>
<div className="text-center logo-container"> <title>{tag.name}</title>
{imageEncoding ? ( </Helmet>
<LoadingIndicator message="Encoding image..." /> <div className="row">
<div className="tag-details col-md-4">
<div className="text-center logo-container">
{imageEncoding ? (
<LoadingIndicator message="Encoding image..." />
) : (
renderImage()
)}
<h2>{tag.name}</h2>
</div>
{!isEditing ? (
<>
<TagDetailsPanel tag={tag} />
{/* HACK - this is also rendered in the TagEditPanel */}
<DetailsEditNavbar
objectName={tag.name}
isNew={false}
isEditing={isEditing}
onToggleEdit={onToggleEdit}
onSave={() => {}}
onImageChange={() => {}}
onClearImage={() => {}}
onAutoTag={onAutoTag}
onDelete={onDelete}
customButtons={renderMergeButton()}
/>
</>
) : ( ) : (
renderImage() <TagEditPanel
)} tag={tag}
<h2>{tag.name}</h2> onSubmit={onSave}
</div> onCancel={onToggleEdit}
{!isEditing ? (
<>
<TagDetailsPanel tag={tag} />
{/* HACK - this is also rendered in the TagEditPanel */}
<DetailsEditNavbar
objectName={tag.name}
isNew={false}
isEditing={isEditing}
onToggleEdit={onToggleEdit}
onSave={() => {}}
onImageChange={() => {}}
onClearImage={() => {}}
onAutoTag={onAutoTag}
onDelete={onDelete} onDelete={onDelete}
customButtons={renderMergeButton()} setImage={setImage}
/> />
</> )}
) : ( </div>
<TagEditPanel <div className="col col-md-8">
tag={tag} <Tabs
onSubmit={onSave} id="tag-tabs"
onCancel={onToggleEdit} mountOnEnter
onDelete={onDelete} activeKey={activeTabKey}
setImage={setImage} onSelect={setActiveTabKey}
/>
)}
</div>
<div className="col col-md-8">
<Tabs
id="tag-tabs"
mountOnEnter
activeKey={activeTabKey}
onSelect={setActiveTabKey}
>
<Tab eventKey="scenes" title={intl.formatMessage({ id: "scenes" })}>
<TagScenesPanel tag={tag} />
</Tab>
<Tab eventKey="images" title={intl.formatMessage({ id: "images" })}>
<TagImagesPanel tag={tag} />
</Tab>
<Tab
eventKey="galleries"
title={intl.formatMessage({ id: "galleries" })}
> >
<TagGalleriesPanel tag={tag} /> <Tab eventKey="scenes" title={intl.formatMessage({ id: "scenes" })}>
</Tab> <TagScenesPanel tag={tag} />
<Tab eventKey="markers" title={intl.formatMessage({ id: "markers" })}> </Tab>
<TagMarkersPanel tag={tag} /> <Tab eventKey="images" title={intl.formatMessage({ id: "images" })}>
</Tab> <TagImagesPanel tag={tag} />
<Tab </Tab>
eventKey="performers" <Tab
title={intl.formatMessage({ id: "performers" })} eventKey="galleries"
> title={intl.formatMessage({ id: "galleries" })}
<TagPerformersPanel tag={tag} /> >
</Tab> <TagGalleriesPanel tag={tag} />
</Tabs> </Tab>
<Tab
eventKey="markers"
title={intl.formatMessage({ id: "markers" })}
>
<TagMarkersPanel tag={tag} />
</Tab>
<Tab
eventKey="performers"
title={intl.formatMessage({ id: "performers" })}
>
<TagPerformersPanel tag={tag} />
</Tab>
</Tabs>
</div>
{renderDeleteAlert()}
{renderMergeDialog()}
</div> </div>
{renderDeleteAlert()} </>
{renderMergeDialog()}
</div>
); );
}; };

View File

@@ -1,15 +1,31 @@
import React from "react"; import React from "react";
import { Route, Switch } from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import { useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import { TITLE_SUFFIX } from "src/components/Shared";
import Tag from "./TagDetails/Tag"; import Tag from "./TagDetails/Tag";
import TagCreate from "./TagDetails/TagCreate"; import TagCreate from "./TagDetails/TagCreate";
import { TagList } from "./TagList"; import { TagList } from "./TagList";
const Tags = () => ( const Tags: React.FC = () => {
<Switch> const intl = useIntl();
<Route exact path="/tags" component={TagList} />
<Route exact path="/tags/new" component={TagCreate} />
<Route path="/tags/:id/:tab?" component={Tag} />
</Switch>
);
const title_template = `${intl.formatMessage({
id: "tags",
})} ${TITLE_SUFFIX}`;
return (
<>
<Helmet
defaultTitle={title_template}
titleTemplate={`%s | ${title_template}`}
/>
<Switch>
<Route exact path="/tags" component={TagList} />
<Route exact path="/tags/new" component={TagCreate} />
<Route path="/tags/:id/:tab?" component={Tag} />
</Switch>
</>
);
};
export default Tags; export default Tags;

View File

@@ -3072,6 +3072,13 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react-helmet@^6.1.3":
version "6.1.3"
resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.3.tgz#1a58b26a79e464c59d3f9cdd5b7ece485335937b"
integrity sha512-U4onVxaZxAp78KpXsfmyCIhLjsvJJ3goG3CYFOo+xW0cPYAz9oe5cBAUSAcN7l35OTbrFvu9TuE0YkcZMKGr4A==
dependencies:
"@types/react" "*"
"@types/react-router-bootstrap@^0.24.5": "@types/react-router-bootstrap@^0.24.5":
version "0.24.5" version "0.24.5"
resolved "https://registry.yarnpkg.com/@types/react-router-bootstrap/-/react-router-bootstrap-0.24.5.tgz#9257ba3dfb01cda201aac9fa05cde3eb09ea5b27" resolved "https://registry.yarnpkg.com/@types/react-router-bootstrap/-/react-router-bootstrap-0.24.5.tgz#9257ba3dfb01cda201aac9fa05cde3eb09ea5b27"
@@ -12635,6 +12642,21 @@ react-fast-compare@^2.0.1:
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
react-fast-compare@^3.1.1:
version "3.2.0"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
react-helmet@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726"
integrity sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==
dependencies:
object-assign "^4.1.1"
prop-types "^15.7.2"
react-fast-compare "^3.1.1"
react-side-effect "^2.1.0"
react-input-autosize@^3.0.0: react-input-autosize@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-3.0.0.tgz#6b5898c790d4478d69420b55441fcc31d5c50a85" resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-3.0.0.tgz#6b5898c790d4478d69420b55441fcc31d5c50a85"
@@ -12836,6 +12858,11 @@ react-select@^4.0.2:
react-input-autosize "^3.0.0" react-input-autosize "^3.0.0"
react-transition-group "^4.3.0" react-transition-group "^4.3.0"
react-side-effect@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.1.tgz#66c5701c3e7560ab4822a4ee2742dee215d72eb3"
integrity sha512-2FoTQzRNTncBVtnzxFOk2mCpcfxQpenBMbk5kSVBg5UcPqV9fRbgY2zhb7GTWWOlpFmAxhClBDlIq8Rsubz1yQ==
react-transition-group@^4.3.0, react-transition-group@^4.4.1: react-transition-group@^4.3.0, react-transition-group@^4.4.1:
version "4.4.1" version "4.4.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"