diff --git a/ui/src/Pages/PeoplePage/PeoplePage.test.tsx b/ui/src/Pages/PeoplePage/PeoplePage.test.tsx
new file mode 100644
index 00000000..0d9db98f
--- /dev/null
+++ b/ui/src/Pages/PeoplePage/PeoplePage.test.tsx
@@ -0,0 +1,102 @@
+import '@testing-library/jest-dom'
+
+import React from 'react'
+import { render, screen, waitFor } from '@testing-library/react'
+import PeoplePage, { MY_FACES_QUERY } from './PeoplePage'
+import { MockedProvider } from '@apollo/client/testing'
+import { MemoryRouter } from 'react-router'
+
+require('../../localization').setupLocalization()
+
+jest.mock('../../hooks/useScrollPagination', () =>
+ jest.fn(() => ({
+ finished: true,
+ containerElem: jest.fn(),
+ }))
+)
+
+const graphqlMocks = [
+ {
+ request: {
+ query: MY_FACES_QUERY,
+ variables: {
+ limit: 50,
+ offset: 0,
+ },
+ },
+ result: {
+ data: {
+ myFaceGroups: [
+ {
+ __typename: 'FaceGroup',
+ id: '3',
+ label: 'Person A',
+ imageFaceCount: 2,
+ imageFaces: [
+ {
+ __typename: 'ImageFace',
+ id: '3',
+ rectangle: {
+ __typename: 'FaceRectangle',
+ minX: 0.2705079913139343,
+ maxX: 0.3408200144767761,
+ minY: 0.7691109776496887,
+ maxY: 0.881434977054596,
+ },
+ media: {
+ __typename: 'Media',
+ id: '63',
+ thumbnail: {
+ __typename: 'MediaURL',
+ url: 'http://localhost:4001/photo/thumbnail_L%C3%B8berute_jpg_p9x8dLWr.jpg',
+ width: 1024,
+ height: 641,
+ },
+ },
+ },
+ ],
+ },
+ {
+ __typename: 'FaceGroup',
+ id: '1',
+ label: 'Person B',
+ imageFaceCount: 1,
+ imageFaces: [],
+ },
+ ],
+ },
+ },
+ },
+]
+
+test('people page', async () => {
+ const matchMock = {
+ params: {
+ person: undefined,
+ },
+ }
+
+ render(
+
+
+
+
+
+ )
+
+ expect(screen.getByTestId('Layout')).toBeInTheDocument()
+ expect(screen.getByText('Recognize unlabeled faces')).toBeInTheDocument()
+
+ await waitFor(() => {
+ expect(screen.getByText('Person A')).toBeInTheDocument()
+ expect(screen.getByText('Person B')).toBeInTheDocument()
+ })
+
+ expect(
+ screen.getAllByRole('link').some(x => x.getAttribute('href') == '/people/1')
+ ).toBeTruthy()
+
+ expect(
+ screen.getAllByRole('link').some(x => x.getAttribute('href') == '/people/3')
+ ).toBeTruthy()
+})
diff --git a/ui/src/Pages/PeoplePage/PeoplePage.js b/ui/src/Pages/PeoplePage/PeoplePage.tsx
similarity index 76%
rename from ui/src/Pages/PeoplePage/PeoplePage.js
rename to ui/src/Pages/PeoplePage/PeoplePage.tsx
index 991447bd..2a625cd2 100644
--- a/ui/src/Pages/PeoplePage/PeoplePage.js
+++ b/ui/src/Pages/PeoplePage/PeoplePage.tsx
@@ -1,5 +1,4 @@
import React, { createRef, useEffect, useState } from 'react'
-import PropTypes from 'prop-types'
import { gql, useMutation, useQuery } from '@apollo/client'
import Layout from '../../Layout'
import styled from 'styled-components'
@@ -10,6 +9,16 @@ import FaceCircleImage from './FaceCircleImage'
import useScrollPagination from '../../hooks/useScrollPagination'
import PaginateLoader from '../../components/PaginateLoader'
import { useTranslation } from 'react-i18next'
+import {
+ setGroupLabel,
+ setGroupLabelVariables,
+} from './__generated__/setGroupLabel'
+import {
+ myFaces,
+ myFacesVariables,
+ myFaces_myFaceGroups,
+} from './__generated__/myFaces'
+import { recognizeUnlabeledFaces } from './__generated__/recognizeUnlabeledFaces'
export const MY_FACES_QUERY = gql`
query myFaces($limit: Int, $offset: Int) {
@@ -55,7 +64,7 @@ const RECOGNIZE_UNLABELED_FACES_MUTATION = gql`
}
`
-const FaceDetailsButton = styled.button`
+const FaceDetailsButton = styled.button<{ labeled: boolean }>`
color: ${({ labeled }) => (labeled ? 'black' : '#aaa')};
width: 150px;
margin: 12px auto 24px;
@@ -73,13 +82,20 @@ const FaceDetailsButton = styled.button`
const FaceLabel = styled.span``
-const FaceDetails = ({ group }) => {
+type FaceDetailsProps = {
+ group: myFaces_myFaceGroups
+}
+
+const FaceDetails = ({ group }: FaceDetailsProps) => {
const { t } = useTranslation()
const [editLabel, setEditLabel] = useState(false)
const [inputValue, setInputValue] = useState(group.label ?? '')
- const inputRef = createRef()
+ const inputRef = createRef()
- const [setGroupLabel, { loading }] = useMutation(SET_GROUP_LABEL_MUTATION, {
+ const [setGroupLabel, { loading }] = useMutation<
+ setGroupLabel,
+ setGroupLabelVariables
+ >(SET_GROUP_LABEL_MUTATION, {
variables: {
groupID: group.id,
},
@@ -91,9 +107,7 @@ const FaceDetails = ({ group }) => {
}
useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus()
- }
+ inputRef.current?.focus()
}, [inputRef])
useEffect(() => {
@@ -102,7 +116,7 @@ const FaceDetails = ({ group }) => {
}
}, [loading])
- const onKeyUp = e => {
+ const onKeyUp = (e: React.ChangeEvent & KeyboardEvent) => {
if (e.key == 'Escape') {
resetLabel()
return
@@ -111,6 +125,7 @@ const FaceDetails = ({ group }) => {
if (e.key == 'Enter') {
setGroupLabel({
variables: {
+ groupID: group.id,
label: e.target.value == '' ? null : e.target.value,
},
})
@@ -155,10 +170,6 @@ const FaceDetails = ({ group }) => {
return label
}
-FaceDetails.propTypes = {
- group: PropTypes.object.isRequired,
-}
-
const FaceImagesCount = styled.span`
background-color: #eee;
color: #222;
@@ -179,7 +190,11 @@ const EditIcon = styled(Icon)`
}
`
-const FaceGroup = ({ group }) => {
+type FaceGroupProps = {
+ group: myFaces_myFaceGroups
+}
+
+const FaceGroup = ({ group }: FaceGroupProps) => {
const previewFace = group.imageFaces[0]
return (
@@ -192,10 +207,6 @@ const FaceGroup = ({ group }) => {
)
}
-FaceGroup.propTypes = {
- group: PropTypes.any,
-}
-
const FaceGroupsWrapper = styled.div`
display: flex;
flex-wrap: wrap;
@@ -204,27 +215,29 @@ const FaceGroupsWrapper = styled.div`
const PeopleGallery = () => {
const { t } = useTranslation()
- const { data, error, loading, fetchMore } = useQuery(MY_FACES_QUERY, {
+ const { data, error, loading, fetchMore } = useQuery<
+ myFaces,
+ myFacesVariables
+ >(MY_FACES_QUERY, {
variables: {
limit: 50,
offset: 0,
},
})
- const [
- recognizeUnlabeled,
- { loading: recognizeUnlabeledLoading },
- ] = useMutation(RECOGNIZE_UNLABELED_FACES_MUTATION)
+ const [recognizeUnlabeled, { loading: recognizeUnlabeledLoading }] =
+ useMutation(RECOGNIZE_UNLABELED_FACES_MUTATION)
- const { containerElem, finished: finishedLoadingMore } = useScrollPagination({
- loading,
- fetchMore,
- data,
- getItems: data => data.myFaceGroups,
- })
+ const { containerElem, finished: finishedLoadingMore } =
+ useScrollPagination({
+ loading,
+ fetchMore,
+ data,
+ getItems: data => data.myFaceGroups,
+ })
if (error) {
- return error.message
+ return {error.message}
}
let faces = null
@@ -258,11 +271,20 @@ const PeopleGallery = () => {
)
}
-const PeoplePage = ({ match }) => {
+type PeoplePageProps = {
+ match: {
+ params: {
+ person?: string
+ }
+ }
+}
+
+const PeoplePage = ({ match }: PeoplePageProps) => {
+ const { t } = useTranslation()
const faceGroup = match.params.person
if (faceGroup) {
return (
-
+
)
@@ -271,8 +293,4 @@ const PeoplePage = ({ match }) => {
}
}
-PeoplePage.propTypes = {
- match: PropTypes.object.isRequired,
-}
-
export default PeoplePage
diff --git a/ui/src/Pages/SharePage/SharePage.test.js b/ui/src/Pages/SharePage/SharePage.test.js
index bd857581..1dbf8d9e 100644
--- a/ui/src/Pages/SharePage/SharePage.test.js
+++ b/ui/src/Pages/SharePage/SharePage.test.js
@@ -110,7 +110,7 @@ describe('load correct share page, based on graphql query', () => {
expect(screen.getByText('Loading...')).toBeInTheDocument()
- await waitForElementToBeRemoved(() => screen.getByText('Loading...'))
+ await waitForElementToBeRemoved(() => screen.queryByText('Loading...'))
expect(screen.getByTestId('Layout')).toBeInTheDocument()
expect(screen.getByTestId('MediaSharePage')).toBeInTheDocument()