mirror of
https://git.vectorsigma.ru/public/photoview.git
synced 2026-08-03 21:09:05 +00:00
Update go and node dependencies.
- Refactor UI to work with v6 of react-router
This commit is contained in:
25838
ui/package-lock.json
generated
25838
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,20 +9,20 @@
|
||||
"license": "GPL-3.0",
|
||||
"description": "UI app for Photoview",
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.3.21",
|
||||
"@babel/preset-typescript": "^7.14.5",
|
||||
"@craco/craco": "^6.2.0",
|
||||
"@headlessui/react": "^1.4.1",
|
||||
"@react-aria/focus": "^3.4.0",
|
||||
"@apollo/client": "^3.5.8",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@craco/craco": "^6.4.3",
|
||||
"@headlessui/react": "^1.4.3",
|
||||
"@react-aria/focus": "^3.5.0",
|
||||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@types/geojson": "^7946.0.8",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/mapbox-gl": "^2.3.3",
|
||||
"@types/react": "^17.0.14",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-helmet": "^6.1.2",
|
||||
"@types/react-router-dom": "^5.1.8",
|
||||
"@types/styled-components": "^5.1.11",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/mapbox-gl": "^2.6.0",
|
||||
"@types/react": "^17.0.38",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/react-helmet": "^6.1.5",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/styled-components": "^5.1.21",
|
||||
"@types/url-join": "^4.0.1",
|
||||
"autoprefixer": "^9.8.6",
|
||||
"babel-plugin-graphql-tag": "^3.3.0",
|
||||
@@ -30,28 +30,28 @@
|
||||
"connect-history-api-fallback": "^1.6.0",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-jest-dom": "^3.9.0",
|
||||
"eslint-plugin-jest-dom": "^4.0.1",
|
||||
"fs-extra": "^10.0.0",
|
||||
"i18next": "^20.3.2",
|
||||
"mapbox-gl": "^2.3.1",
|
||||
"i18next": "^21.6.10",
|
||||
"mapbox-gl": "^2.6.1",
|
||||
"postcss": "^7.0.36",
|
||||
"prettier": "^2.3.2",
|
||||
"prop-types": "^15.7.2",
|
||||
"prettier": "^2.5.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hook-form": "^7.11.0",
|
||||
"react-i18next": "^11.11.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-hook-form": "^7.25.3",
|
||||
"react-i18next": "^11.15.3",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"react-router-prop-types": "^1.0.5",
|
||||
"react-scripts": "^4.0.3",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"styled-components": "^5.3.0",
|
||||
"subscriptions-transport-ws": "^0.9.19",
|
||||
"tailwind-override": "^0.2.3",
|
||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4",
|
||||
"typescript": "^4.3.5",
|
||||
"styled-components": "^5.3.3",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
"tailwind-override": "^0.6.1",
|
||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.17",
|
||||
"typescript": "^4.5.5",
|
||||
"url-join": "^4.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -69,15 +69,15 @@
|
||||
"prepare": "(cd .. && npx husky install)"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"@testing-library/user-event": "^13.1.9",
|
||||
"apollo": "2.33.4",
|
||||
"apollo-language-server": "1.26.3",
|
||||
"husky": "^6.0.0",
|
||||
"i18next-parser": "^4.2.0",
|
||||
"lint-staged": "^11.0.1",
|
||||
"tsc-files": "^1.1.2"
|
||||
"@testing-library/jest-dom": "^5.16.1",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"apollo": "2.33.9",
|
||||
"apollo-language-server": "1.26.7",
|
||||
"husky": "^7.0.4",
|
||||
"i18next-parser": "^5.4.0",
|
||||
"lint-staged": "^12.3.2",
|
||||
"tsc-files": "1.1.2"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
|
||||
@@ -8,6 +8,8 @@ import PaginateLoader from '../../components/PaginateLoader'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { albumQuery, albumQueryVariables } from './__generated__/albumQuery'
|
||||
import useOrderingParams from '../../hooks/useOrderingParams'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { isNil } from '../../helpers/utils'
|
||||
|
||||
const ALBUM_QUERY = gql`
|
||||
query albumQuery(
|
||||
@@ -61,17 +63,10 @@ const ALBUM_QUERY = gql`
|
||||
let refetchNeededAll = false
|
||||
let refetchNeededFavorites = false
|
||||
|
||||
type AlbumPageProps = {
|
||||
match: {
|
||||
params: {
|
||||
id: string
|
||||
subPage: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function AlbumPage({ match }: AlbumPageProps) {
|
||||
const albumId = match.params.id
|
||||
function AlbumPage() {
|
||||
const { id: albumId } = useParams()
|
||||
if (isNil(albumId))
|
||||
throw new Error('Expected parameter `id` to be defined for AlbumPage')
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import { gql, useQuery, useMutation } from '@apollo/client'
|
||||
import { Redirect } from 'react-router-dom'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Container } from './loginUtilities'
|
||||
|
||||
import { checkInitialSetupQuery, login } from './loginUtilities'
|
||||
@@ -37,6 +37,7 @@ type InitialSetupFormData = {
|
||||
|
||||
const InitialSetupPage = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -45,16 +46,18 @@ const InitialSetupPage = () => {
|
||||
} = useForm<InitialSetupFormData>()
|
||||
|
||||
if (authToken()) {
|
||||
return <Redirect to="/" />
|
||||
navigate('/')
|
||||
return null
|
||||
}
|
||||
|
||||
const { data: initialSetupData } = useQuery<CheckInitialSetup>(
|
||||
checkInitialSetupQuery
|
||||
)
|
||||
const initialSetupRedirect = initialSetupData?.siteInfo
|
||||
?.initialSetup ? null : (
|
||||
<Redirect to="/" />
|
||||
)
|
||||
|
||||
if (initialSetupData?.siteInfo?.initialSetup) {
|
||||
navigate('/')
|
||||
return null
|
||||
}
|
||||
|
||||
const [authorize, { loading: authorizeLoading, data: authorizationData }] =
|
||||
useMutation(initialSetupMutation, {
|
||||
@@ -84,7 +87,6 @@ const InitialSetupPage = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{initialSetupRedirect}
|
||||
<Container>
|
||||
<h1 className="text-center text-xl">
|
||||
{t('login_page.initial_setup.title', 'Initial Setup')}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react'
|
||||
import { useQuery, gql, useMutation } from '@apollo/client'
|
||||
import { useForm, SubmitHandler } from 'react-hook-form'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { checkInitialSetupQuery, login } from './loginUtilities'
|
||||
import { authToken } from '../../helpers/authentication'
|
||||
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { Redirect } from 'react-router'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { TextField } from '../../primitives/form/Input'
|
||||
import MessageBox from '../../primitives/form/MessageBox'
|
||||
import { CheckInitialSetup } from './__generated__/CheckInitialSetup'
|
||||
@@ -45,7 +45,7 @@ const LoginForm = () => {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors: formErrors },
|
||||
} = useForm()
|
||||
} = useForm<LoginInputs>()
|
||||
|
||||
const [authorize, { loading, data }] = useMutation<
|
||||
Authorize,
|
||||
@@ -60,7 +60,7 @@ const LoginForm = () => {
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit: SubmitHandler<LoginInputs> = data => {
|
||||
const onSubmit = (data: LoginInputs) => {
|
||||
authorize({
|
||||
variables: {
|
||||
username: data.username,
|
||||
@@ -127,13 +127,20 @@ type LoginInputs = {
|
||||
|
||||
const LoginPage = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { data: initialSetupData } = useQuery<CheckInitialSetup>(
|
||||
checkInitialSetupQuery
|
||||
)
|
||||
|
||||
if (authToken()) {
|
||||
return <Redirect to="/" />
|
||||
navigate('/')
|
||||
return null
|
||||
}
|
||||
|
||||
if (initialSetupData?.siteInfo?.initialSetup) {
|
||||
navigate('initialSetup')
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -141,9 +148,6 @@ const LoginPage = () => {
|
||||
<Helmet>
|
||||
<title>{t('title.login', 'Login')} - Photoview</title>
|
||||
</Helmet>
|
||||
{initialSetupData?.siteInfo?.initialSetup && (
|
||||
<Redirect to="/initialSetup" />
|
||||
)}
|
||||
<div>
|
||||
<LogoHeader />
|
||||
<LoginForm />
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { createRef, useEffect, useState } from 'react'
|
||||
import { gql, useMutation, useQuery } from '@apollo/client'
|
||||
import Layout from '../../components/layout/Layout'
|
||||
import styled from 'styled-components'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import SingleFaceGroup from './SingleFaceGroup/SingleFaceGroup'
|
||||
import { Button, TextField } from '../../primitives/form/Input'
|
||||
import FaceCircleImage from './FaceCircleImage'
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
myFaces_myFaceGroups,
|
||||
} from './__generated__/myFaces'
|
||||
import { recognizeUnlabeledFaces } from './__generated__/recognizeUnlabeledFaces'
|
||||
import { tailwindClassNames } from '../../helpers/utils'
|
||||
import { isNil, tailwindClassNames } from '../../helpers/utils'
|
||||
|
||||
export const MY_FACES_QUERY = gql`
|
||||
query myFaces($limit: Int, $offset: Int) {
|
||||
@@ -219,7 +219,10 @@ const FaceGroupsWrapper = styled.div`
|
||||
margin-top: 24px;
|
||||
`
|
||||
|
||||
const PeopleGallery = () => {
|
||||
/**
|
||||
* Renders a page that shows a gallery of all people
|
||||
*/
|
||||
export const PeoplePage = () => {
|
||||
const { t } = useTranslation()
|
||||
const { data, error, loading, fetchMore } = useQuery<
|
||||
myFaces,
|
||||
@@ -275,26 +278,18 @@ const PeopleGallery = () => {
|
||||
)
|
||||
}
|
||||
|
||||
type PeoplePageProps = {
|
||||
match: {
|
||||
params: {
|
||||
person?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const PeoplePage = ({ match }: PeoplePageProps) => {
|
||||
/**
|
||||
* Renders a page for an individual person
|
||||
*/
|
||||
export const PersonPage = () => {
|
||||
const { t } = useTranslation()
|
||||
const faceGroup = match.params.person
|
||||
if (faceGroup) {
|
||||
return (
|
||||
<Layout title={t('title.people', 'People')}>
|
||||
<SingleFaceGroup faceGroupID={faceGroup} />
|
||||
</Layout>
|
||||
)
|
||||
} else {
|
||||
return <PeopleGallery />
|
||||
}
|
||||
}
|
||||
const { person } = useParams()
|
||||
if (isNil(person))
|
||||
throw new Error('Expected `person` parameter to be defined')
|
||||
|
||||
export default PeoplePage
|
||||
return (
|
||||
<Layout title={t('title.people', 'People')}>
|
||||
<SingleFaceGroup faceGroupID={person} />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BaseMutationOptions, gql, useMutation } from '@apollo/client'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { isNil } from '../../../helpers/utils'
|
||||
import Modal from '../../../primitives/Modal'
|
||||
import { MY_FACES_QUERY } from '../PeoplePage'
|
||||
@@ -78,7 +78,7 @@ const DetachImageFacesModal = ({
|
||||
const [selectedImageFaces, setSelectedImageFaces] = useState<
|
||||
(myFaces_myFaceGroups_imageFaces | singleFaceGroup_faceGroup_imageFaces)[]
|
||||
>([])
|
||||
const history = useHistory()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const detachImageFacesMutation = useDetachImageFaces({
|
||||
refetchQueries: [
|
||||
@@ -92,7 +92,7 @@ const DetachImageFacesModal = ({
|
||||
detachImageFacesMutation(selectedImageFaces).then(({ data }) => {
|
||||
if (isNil(data)) throw new Error('Expected data not to be null')
|
||||
setOpen(false)
|
||||
history.push(`/people/${data.detachImageFaces.id}`)
|
||||
navigate(`/people/${data.detachImageFaces.id}`)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { gql, PureQueryOptions, useMutation, useQuery } from '@apollo/client'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { isNil } from '../../../helpers/utils'
|
||||
import Modal from '../../../primitives/Modal'
|
||||
import { MY_FACES_QUERY } from '../PeoplePage'
|
||||
@@ -50,7 +50,7 @@ const MergeFaceGroupsModal = ({
|
||||
myFaces_myFaceGroups | singleFaceGroup_faceGroup | null
|
||||
>(null)
|
||||
|
||||
const history = useHistory()
|
||||
const navigate = useNavigate()
|
||||
const { data } = useQuery<myFaces, myFacesVariables>(MY_FACES_QUERY)
|
||||
const [combineFacesMutation] = useMutation<
|
||||
combineFaces,
|
||||
@@ -74,7 +74,7 @@ const MergeFaceGroupsModal = ({
|
||||
},
|
||||
}).then(() => {
|
||||
setOpen(false)
|
||||
history.push(`/people/${selectedFaceGroup.id}`)
|
||||
navigate(`/people/${selectedFaceGroup.id}`)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { gql, useLazyQuery, useMutation } from '@apollo/client'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import SelectFaceGroupTable from './SelectFaceGroupTable'
|
||||
import SelectImageFacesTable from './SelectImageFacesTable'
|
||||
import { MY_FACES_QUERY } from '../PeoplePage'
|
||||
@@ -61,7 +61,7 @@ const MoveImageFacesModal = ({
|
||||
myFaces_myFaceGroups | singleFaceGroup_faceGroup | null
|
||||
>(null)
|
||||
const [imagesSelected, setImagesSelected] = useState(false)
|
||||
const history = useHistory()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [moveImageFacesMutation] = useMutation<
|
||||
moveImageFaces,
|
||||
@@ -115,7 +115,7 @@ const MoveImageFacesModal = ({
|
||||
},
|
||||
}).then(() => {
|
||||
setOpen(false)
|
||||
history.push(`/people/${selectedFaceGroup.id}`)
|
||||
navigate(`/people/${selectedFaceGroup.id}`)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
27
ui/src/Pages/PeoplePage/peoplePageRoute.tsx
Normal file
27
ui/src/Pages/PeoplePage/peoplePageRoute.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Light lazy-loadable wrapper for the people module
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { Route } from 'react-router-dom'
|
||||
|
||||
const PeoplePage = React.lazy(() =>
|
||||
import('./PeoplePage').then(x => ({
|
||||
default: x.PeoplePage,
|
||||
}))
|
||||
)
|
||||
|
||||
const PersonPage = React.lazy(() =>
|
||||
import('./PeoplePage').then(x => ({
|
||||
default: x.PersonPage,
|
||||
}))
|
||||
)
|
||||
|
||||
const peoplePageRoute = () => (
|
||||
<>
|
||||
<Route path=":person" element={<PersonPage />} />
|
||||
<Route index element={<PeoplePage />} />
|
||||
</>
|
||||
)
|
||||
|
||||
export default peoplePageRoute
|
||||
@@ -1,7 +1,6 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { useQuery, gql } from '@apollo/client'
|
||||
import { match as MatchType, Route, Switch } from 'react-router-dom'
|
||||
import { Route, useParams } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
import {
|
||||
getSharePassword,
|
||||
@@ -11,6 +10,7 @@ import AlbumSharePage from './AlbumSharePage'
|
||||
import MediaSharePage from './MediaSharePage'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PasswordProtectedShare from './PasswordProtectedShare'
|
||||
import { isNil } from '../../helpers/utils'
|
||||
|
||||
export const SHARE_TOKEN_QUERY = gql`
|
||||
query SharePageToken($token: String!, $password: String) {
|
||||
@@ -77,10 +77,16 @@ export const VALIDATE_TOKEN_PASSWORD_QUERY = gql`
|
||||
}
|
||||
`
|
||||
|
||||
const AuthorizedTokenRoute = ({ match }: MatchProps<TokenRouteMatch>) => {
|
||||
const tokenFromParams = () => {
|
||||
const { token } = useParams()
|
||||
if (isNil(token)) throw new Error('Expected `token` param to be defined')
|
||||
return token
|
||||
}
|
||||
|
||||
const AuthorizedTokenRoute = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const token = match.params.token
|
||||
const token = tokenFromParams()
|
||||
const password = getSharePassword(token)
|
||||
|
||||
const { loading, error, data } = useQuery(SHARE_TOKEN_QUERY, {
|
||||
@@ -94,35 +100,30 @@ const AuthorizedTokenRoute = ({ match }: MatchProps<TokenRouteMatch>) => {
|
||||
if (loading) return <div>{t('general.loading.default', 'Loading...')}</div>
|
||||
|
||||
if (data.shareToken.album) {
|
||||
const SharedSubAlbumPage = ({ match }: MatchProps<SubalbumRouteMatch>) => {
|
||||
const SharedSubAlbumPage = () => {
|
||||
const { subAlbum } = useParams()
|
||||
if (isNil(subAlbum))
|
||||
throw new Error('Expected `subAlbum` param to be defined')
|
||||
|
||||
return (
|
||||
<AlbumSharePage
|
||||
albumID={match.params.subAlbum}
|
||||
token={token}
|
||||
password={password}
|
||||
/>
|
||||
<AlbumSharePage albumID={subAlbum} token={token} password={password} />
|
||||
)
|
||||
}
|
||||
|
||||
SharedSubAlbumPage.propTypes = {
|
||||
match: PropTypes.any,
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<>
|
||||
<Route path=":subAlbum" element={<SharedSubAlbumPage />} />
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/:subAlbum`}
|
||||
component={SharedSubAlbumPage}
|
||||
path="/"
|
||||
element={
|
||||
<AlbumSharePage
|
||||
albumID={data.shareToken.album.id}
|
||||
token={token}
|
||||
password={password}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route exact path={match.path}>
|
||||
<AlbumSharePage
|
||||
albumID={data.shareToken.album.id}
|
||||
token={token}
|
||||
password={password}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -133,39 +134,22 @@ const AuthorizedTokenRoute = ({ match }: MatchProps<TokenRouteMatch>) => {
|
||||
return <h1>{t('share_page.share_not_found', 'Share not found')}</h1>
|
||||
}
|
||||
|
||||
AuthorizedTokenRoute.propTypes = {
|
||||
match: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
export const MessageContainer = styled.div`
|
||||
max-width: 400px;
|
||||
margin: 100px auto 0;
|
||||
`
|
||||
|
||||
interface TokenRouteMatch {
|
||||
token: string
|
||||
}
|
||||
|
||||
interface SubalbumRouteMatch extends TokenRouteMatch {
|
||||
subAlbum: string
|
||||
}
|
||||
|
||||
interface MatchProps<Route> {
|
||||
match: MatchType<Route>
|
||||
}
|
||||
|
||||
const TokenRoute = ({ match }: MatchProps<TokenRouteMatch>) => {
|
||||
export const TokenRoute = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const token = match.params.token
|
||||
const token = tokenFromParams()
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(
|
||||
VALIDATE_TOKEN_PASSWORD_QUERY,
|
||||
{
|
||||
notifyOnNetworkStatusChange: true,
|
||||
variables: {
|
||||
token: match.params.token,
|
||||
password: getSharePassword(match.params.token),
|
||||
token: token,
|
||||
password: getSharePassword(token),
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -202,22 +186,5 @@ const TokenRoute = ({ match }: MatchProps<TokenRouteMatch>) => {
|
||||
|
||||
if (loading) return <div>{t('general.loading.default', 'Loading...')}</div>
|
||||
|
||||
return <AuthorizedTokenRoute match={match} />
|
||||
return <AuthorizedTokenRoute />
|
||||
}
|
||||
|
||||
const SharePage = ({ match }: { match: MatchType }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={`${match.url}/:token`}>
|
||||
{({ match }: { match: MatchType<TokenRouteMatch> }) => {
|
||||
return <TokenRoute match={match} />
|
||||
}}
|
||||
</Route>
|
||||
<Route path="/">{t('routes.page_not_found', 'Page not found')}</Route>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
export default SharePage
|
||||
|
||||
25
ui/src/Pages/SharePage/sharePageRoute.tsx
Normal file
25
ui/src/Pages/SharePage/sharePageRoute.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Light lazy-loadable wrapper for share page module
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import type { TFunction } from 'react-i18next'
|
||||
import { Route } from 'react-router-dom'
|
||||
import { NotFoundPage } from '../../components/routes/Routes'
|
||||
|
||||
const TokenRoute = React.lazy(() =>
|
||||
import('./SharePage').then(x => ({
|
||||
default: x.TokenRoute,
|
||||
}))
|
||||
)
|
||||
|
||||
const sharePageRoute = ({ t }: { t: TFunction }) => {
|
||||
return (
|
||||
<>
|
||||
<Route path={':token'} element={<TokenRoute />} />
|
||||
<Route index element={<NotFoundPage t={t} />} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default sharePageRoute
|
||||
@@ -3,7 +3,7 @@ import styled from 'styled-components'
|
||||
import { useLazyQuery, gql } from '@apollo/client'
|
||||
import { debounce, DebouncedFn } from '../../helpers/utils'
|
||||
import { ProtectedImage } from '../photoGallery/ProtectedMedia'
|
||||
import { NavLink, useHistory, useRouteMatch } from 'react-router-dom'
|
||||
import { NavLink, useNavigate, useLocation } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
searchQuery,
|
||||
@@ -77,11 +77,11 @@ const SearchBar = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const routerMatch = useRouteMatch()
|
||||
const location = useLocation()
|
||||
useEffect(() => {
|
||||
setExpanded(false)
|
||||
setQuery('')
|
||||
}, [routerMatch])
|
||||
}, [location])
|
||||
|
||||
const [selectedItem, setSelectedItem] = useState<number | null>(null)
|
||||
|
||||
@@ -291,13 +291,11 @@ const SearchRow = ({
|
||||
setSelected,
|
||||
}: SearchRowProps) => {
|
||||
const rowEl = useRef<HTMLLIElement>(null)
|
||||
const history = useHistory()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
const keydownEvent = (event: KeyboardEvent) => {
|
||||
if (event.key == 'Enter') {
|
||||
history.push(link)
|
||||
}
|
||||
if (event.key == 'Enter') navigate(link)
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', keydownEvent)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useQuery, gql } from '@apollo/client'
|
||||
import { authToken } from '../../helpers/authentication'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { mapboxEnabledQuery } from '../../__generated__/mapboxEnabledQuery'
|
||||
import classNames from 'classnames'
|
||||
|
||||
export const MAPBOX_QUERY = gql`
|
||||
query mapboxEnabledQuery {
|
||||
@@ -41,9 +42,12 @@ const MenuButton = ({
|
||||
return (
|
||||
<NavLink
|
||||
to={to}
|
||||
exact={exact}
|
||||
className={`rounded-lg my-2 ${className}`}
|
||||
activeClassName={`ring-4 lg:ring-4 ${activeClasses}`}
|
||||
end={exact}
|
||||
className={({ isActive }) =>
|
||||
classNames(`rounded-lg my-2`, className, {
|
||||
[`ring-4 lg:ring-4 ${activeClasses}`]: isActive,
|
||||
})
|
||||
}
|
||||
>
|
||||
<li className="flex items-center">
|
||||
<div
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import React, { ReactChild, useEffect } from 'react'
|
||||
import PropTypes, { ReactComponentLike } from 'prop-types'
|
||||
import { Route, Redirect, RouteProps } from 'react-router-dom'
|
||||
import React, { useEffect } from 'react'
|
||||
import { RouteProps, Navigate } from 'react-router-dom'
|
||||
import { useLazyQuery } from '@apollo/client'
|
||||
import { authToken } from '../../helpers/authentication'
|
||||
import { ADMIN_QUERY } from '../layout/Layout'
|
||||
|
||||
export const useIsAdmin = (enabled = true) => {
|
||||
export const useIsAdmin = () => {
|
||||
const [fetchAdminQuery, { data, called }] = useLazyQuery(ADMIN_QUERY)
|
||||
|
||||
useEffect(() => {
|
||||
if (authToken() && !called && enabled) {
|
||||
if (authToken() && !called) {
|
||||
fetchAdminQuery()
|
||||
}
|
||||
}, [authToken(), enabled])
|
||||
}, [authToken()])
|
||||
|
||||
if (!authToken()) {
|
||||
return false
|
||||
@@ -28,47 +27,23 @@ export const Authorized = ({ children }: { children: JSX.Element }) => {
|
||||
}
|
||||
|
||||
interface AuthorizedRouteProps extends Omit<RouteProps, 'component'> {
|
||||
component: ReactComponentLike
|
||||
children: React.ReactNode
|
||||
admin?: boolean
|
||||
}
|
||||
|
||||
const AuthorizedRoute = ({
|
||||
component: Component,
|
||||
admin = false,
|
||||
...props
|
||||
}: AuthorizedRouteProps) => {
|
||||
const AuthorizedRoute = ({ admin = false, children }: AuthorizedRouteProps) => {
|
||||
const token = authToken()
|
||||
const isAdmin = useIsAdmin(admin)
|
||||
const isAdmin = useIsAdmin()
|
||||
|
||||
let unauthorizedRedirect: null | ReactChild = null
|
||||
if (!token) {
|
||||
unauthorizedRedirect = <Redirect to="/login" />
|
||||
return <Navigate to="/" />
|
||||
}
|
||||
|
||||
let adminRedirect: null | ReactChild = null
|
||||
if (token && admin) {
|
||||
if (isAdmin === false) {
|
||||
adminRedirect = <Redirect to="/" />
|
||||
}
|
||||
if (admin && !isAdmin) {
|
||||
return <Navigate to="/" />
|
||||
}
|
||||
|
||||
return (
|
||||
<Route
|
||||
{...props}
|
||||
render={routeProps => (
|
||||
<>
|
||||
{unauthorizedRedirect}
|
||||
{adminRedirect}
|
||||
<Component {...routeProps} />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
AuthorizedRoute.propTypes = {
|
||||
component: PropTypes.elementType.isRequired,
|
||||
admin: PropTypes.bool,
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
export default AuthorizedRoute
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import React from 'react'
|
||||
import { Route, Switch, Redirect } from 'react-router-dom'
|
||||
import {
|
||||
Route,
|
||||
Routes as RouterRoutes,
|
||||
Navigate,
|
||||
useNavigate,
|
||||
NavigateFunction,
|
||||
Outlet,
|
||||
} from 'react-router-dom'
|
||||
|
||||
import Layout from '../layout/Layout'
|
||||
import { clearTokenCookie } from '../../helpers/authentication'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { authToken, clearTokenCookie } from '../../helpers/authentication'
|
||||
import { TFunction, useTranslation } from 'react-i18next'
|
||||
import Loader from '../../primitives/Loader'
|
||||
|
||||
const AuthorizedRoute = React.lazy(() => import('./AuthorizedRoute'))
|
||||
import AuthorizedRoute from './AuthorizedRoute'
|
||||
import sharePageRoute from '../../Pages/SharePage/sharePageRoute'
|
||||
import peoplePageRoute from '../../Pages/PeoplePage/peoplePageRoute'
|
||||
|
||||
const AlbumsPage = React.lazy(
|
||||
() => import('../../Pages/AllAlbumsPage/AlbumsPage')
|
||||
@@ -16,8 +24,6 @@ const TimelinePage = React.lazy(
|
||||
() => import('../../Pages/TimelinePage/TimelinePage')
|
||||
)
|
||||
const PlacesPage = React.lazy(() => import('../../Pages/PlacesPage/PlacesPage'))
|
||||
const SharePage = React.lazy(() => import('../../Pages/SharePage/SharePage'))
|
||||
const PeoplePage = React.lazy(() => import('../../Pages/PeoplePage/PeoplePage'))
|
||||
|
||||
const LoginPage = React.lazy(() => import('../../Pages/LoginPage/LoginPage'))
|
||||
const InitialSetupPage = React.lazy(
|
||||
@@ -30,6 +36,7 @@ const SettingsPage = React.lazy(
|
||||
|
||||
const Routes = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<React.Suspense
|
||||
@@ -39,37 +46,86 @@ const Routes = () => {
|
||||
</Layout>
|
||||
}
|
||||
>
|
||||
<Switch>
|
||||
<Route path="/login" component={LoginPage} />
|
||||
<Route path="/logout">
|
||||
{() => {
|
||||
clearTokenCookie()
|
||||
location.href = '/'
|
||||
}}
|
||||
<RouterRoutes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/logout" element={<LogoutPage navigate={navigate} />} />
|
||||
<Route path="/initialSetup" element={<InitialSetupPage />} />
|
||||
<Route path="/share">{sharePageRoute({ t })}</Route>
|
||||
<Route
|
||||
path="/albums"
|
||||
element={
|
||||
<AuthorizedRoute>
|
||||
<AlbumsPage />
|
||||
</AuthorizedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/album/:id"
|
||||
element={
|
||||
<AuthorizedRoute>
|
||||
<AlbumPage />
|
||||
</AuthorizedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/timeline"
|
||||
element={
|
||||
<AuthorizedRoute>
|
||||
<TimelinePage />
|
||||
</AuthorizedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/places"
|
||||
element={
|
||||
<AuthorizedRoute>
|
||||
<PlacesPage />
|
||||
</AuthorizedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/people"
|
||||
element={
|
||||
<AuthorizedRoute>
|
||||
<Outlet />
|
||||
</AuthorizedRoute>
|
||||
}
|
||||
>
|
||||
{peoplePageRoute()}
|
||||
</Route>
|
||||
<Route path="/initialSetup" component={InitialSetupPage} />
|
||||
<Route path="/share" component={SharePage} />
|
||||
<AuthorizedRoute exact path="/albums" component={AlbumsPage} />
|
||||
<AuthorizedRoute path="/album/:id" component={AlbumPage} />
|
||||
<AuthorizedRoute path="/timeline" component={TimelinePage} />
|
||||
<AuthorizedRoute path="/places" component={PlacesPage} />
|
||||
<AuthorizedRoute path="/people/:person?" component={PeoplePage} />
|
||||
<AuthorizedRoute path="/settings" component={SettingsPage} />
|
||||
<Route path="/" exact render={() => <Redirect to="/timeline" />} />
|
||||
<Route
|
||||
path="/settings"
|
||||
element={
|
||||
<AuthorizedRoute>
|
||||
<SettingsPage />
|
||||
</AuthorizedRoute>
|
||||
}
|
||||
/>
|
||||
<Route index element={<IndexPage />} />
|
||||
{/* For backwards compatibility */}
|
||||
<Route
|
||||
path="/photos"
|
||||
exact
|
||||
render={() => <Redirect to="/timeline" />}
|
||||
/>
|
||||
<Route
|
||||
render={() => (
|
||||
<div>{t('routes.page_not_found', 'Page not found')}</div>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
<Route path="/photos" element={<Navigate to="/timeline" />} />
|
||||
<Route element={<NotFoundPage t={t} />} />
|
||||
</RouterRoutes>
|
||||
</React.Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
const IndexPage = () => {
|
||||
const token = authToken()
|
||||
|
||||
const dest = token ? '/timeline' : '/login'
|
||||
|
||||
return <Navigate to={dest} />
|
||||
}
|
||||
|
||||
export const NotFoundPage = ({ t }: { t: TFunction }) => {
|
||||
return <div>{t('routes.page_not_found', 'Page not found')}</div>
|
||||
}
|
||||
|
||||
const LogoutPage = ({ navigate }: { navigate: NavigateFunction }) => {
|
||||
clearTokenCookie()
|
||||
navigate('/')
|
||||
return null
|
||||
}
|
||||
|
||||
export default Routes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link, useHistory } from 'react-router-dom'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import FaceCircleImage from '../../../Pages/PeoplePage/FaceCircleImage'
|
||||
import { SidebarSection, SidebarSectionTitle } from '../SidebarComponents'
|
||||
import { MediaSidebarMedia, SIDEBAR_MEDIA_QUERY } from './MediaSidebar'
|
||||
@@ -73,7 +73,7 @@ const PersonMoreMenu = ({
|
||||
},
|
||||
]
|
||||
|
||||
const history = useHistory()
|
||||
const navigate = useNavigate()
|
||||
const detachImageFaceMutation = useDetachImageFaces({
|
||||
refetchQueries,
|
||||
})
|
||||
@@ -107,7 +107,7 @@ const PersonMoreMenu = ({
|
||||
return
|
||||
detachImageFaceMutation([face]).then(({ data }) => {
|
||||
if (isNil(data)) throw new Error('Expected data not to be null')
|
||||
history.push(`/people/${data.detachImageFaces.id}`)
|
||||
navigate(`/people/${data.detachImageFaces.id}`)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ const CheckboxLabelWrapper = styled.label`
|
||||
|
||||
type CheckboxProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
label: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Checkbox = ({ label, className, ...props }: CheckboxProps) => {
|
||||
|
||||
@@ -19,6 +19,7 @@ type DropdownProps = React.SelectHTMLAttributes<HTMLSelectElement> & {
|
||||
items: DropdownItem[]
|
||||
selected?: string
|
||||
setSelected(value: string): void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Dropdown = ({
|
||||
|
||||
@@ -141,6 +141,7 @@ export const TextField = forwardRef(
|
||||
type ButtonProps = {
|
||||
variant?: 'negative' | 'positive' | 'default'
|
||||
background?: 'default' | 'white'
|
||||
className?: string
|
||||
}
|
||||
|
||||
const buttonStyles = ({ variant, background }: ButtonProps) =>
|
||||
@@ -155,6 +156,7 @@ const buttonStyles = ({ variant, background }: ButtonProps) =>
|
||||
|
||||
type SubmitProps = ButtonProps & {
|
||||
children: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const Submit = ({
|
||||
|
||||
Reference in New Issue
Block a user