From cd04d34cab268e9f08a77c9f41a9f840d0d951e8 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Tue, 18 May 2021 18:05:34 +0200 Subject: [PATCH 1/6] Rewrite PeoplePage in Typescript and add tests --- ui/src/Pages/PeoplePage/PeoplePage.test.tsx | 102 ++++++++++++++++++ .../{PeoplePage.js => PeoplePage.tsx} | 90 +++++++++------- ui/src/Pages/SharePage/SharePage.test.js | 2 +- 3 files changed, 157 insertions(+), 37 deletions(-) create mode 100644 ui/src/Pages/PeoplePage/PeoplePage.test.tsx rename ui/src/Pages/PeoplePage/{PeoplePage.js => PeoplePage.tsx} (76%) 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() From 3c0690c546fec8577851163b3735081cb2f7fbf6 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Wed, 19 May 2021 17:46:50 +0200 Subject: [PATCH 2/6] Rewrite FaceCircleImage to TypeScript --- ui/src/Pages/PeoplePage/FaceCircleImage.js | 101 ----------------- .../SingleFaceGroup/SingleFaceGroup.js | 106 ------------------ 2 files changed, 207 deletions(-) delete mode 100644 ui/src/Pages/PeoplePage/FaceCircleImage.js delete mode 100644 ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.js diff --git a/ui/src/Pages/PeoplePage/FaceCircleImage.js b/ui/src/Pages/PeoplePage/FaceCircleImage.js deleted file mode 100644 index d77dc148..00000000 --- a/ui/src/Pages/PeoplePage/FaceCircleImage.js +++ /dev/null @@ -1,101 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import styled from 'styled-components' -import { ProtectedImage } from '../../components/photoGallery/ProtectedMedia' - -const FaceImage = styled(ProtectedImage)` - position: absolute; - transform-origin: ${({ $origin }) => - `${$origin.x * 100}% ${$origin.y * 100}%`}; - object-fit: cover; - - transition: transform 250ms ease-out; -` - -const FaceImagePortrait = styled(FaceImage)` - width: 100%; - top: 50%; - transform: translateY(-50%) - ${({ $origin, $scale }) => - `translate(${(0.5 - $origin.x) * 100}%, ${ - (0.5 - $origin.y) * 100 - }%) scale(${Math.max($scale * 0.8, 1)})`}; - - ${({ $selectable, $origin, $scale }) => - $selectable - ? ` - &:hover { - transform: translateY(-50%) translate(${(0.5 - $origin.x) * 100}%, ${ - (0.5 - $origin.y) * 100 - }%) scale(${Math.max($scale * 0.85, 1)}) - ` - : ''} -` - -const FaceImageLandscape = styled(FaceImage)` - height: 100%; - left: 50%; - transform: translateX(-50%) - ${({ $origin, $scale }) => - `translate(${(0.5 - $origin.x) * 100}%, ${ - (0.5 - $origin.y) * 100 - }%) scale(${Math.max($scale * 0.8, 1)})`}; - - ${({ $selectable, $origin, $scale }) => - $selectable - ? ` - &:hover { - transform: translateX(-50%) translate(${(0.5 - $origin.x) * 100}%, ${ - (0.5 - $origin.y) * 100 - }%) scale(${Math.max($scale * 0.85, 1)}) - ` - : ''} -` - -const CircleImageWrapper = styled.div` - background-color: #eee; - position: relative; - border-radius: 50%; - width: ${({ size }) => size}; - height: ${({ size }) => size}; - object-fit: fill; - overflow: hidden; -` - -const FaceCircleImage = ({ imageFace, selectable, size = '150px' }) => { - if (!imageFace) { - return null - } - - const rect = imageFace.rectangle - - let scale = Math.min(1 / (rect.maxX - rect.minX), 1 / (rect.maxY - rect.minY)) - - let origin = { - x: (rect.minX + rect.maxX) / 2, - y: (rect.minY + rect.maxY) / 2, - } - - const SpecificFaceImage = - imageFace.media.thumbnail.width > imageFace.media.thumbnail.height - ? FaceImageLandscape - : FaceImagePortrait - return ( - - - - ) -} - -FaceCircleImage.propTypes = { - imageFace: PropTypes.object, - selectable: PropTypes.bool, - size: PropTypes.string, -} - -export default FaceCircleImage diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.js b/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.js deleted file mode 100644 index 60ab14cd..00000000 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.js +++ /dev/null @@ -1,106 +0,0 @@ -import { gql, useQuery } from '@apollo/client' -import PropTypes from 'prop-types' -import React, { useState } from 'react' -import PaginateLoader from '../../../components/PaginateLoader' -import PhotoGallery from '../../../components/photoGallery/PhotoGallery' -import useScrollPagination from '../../../hooks/useScrollPagination' -import FaceGroupTitle from './FaceGroupTitle' - -export const SINGLE_FACE_GROUP = gql` - query singleFaceGroup($id: ID!, $limit: Int!, $offset: Int!) { - faceGroup(id: $id) { - id - label - imageFaces(paginate: { limit: $limit, offset: $offset }) { - id - rectangle { - minX - maxX - minY - maxY - } - media { - id - type - title - thumbnail { - url - width - height - } - highRes { - url - } - favorite - } - } - } - } -` - -const SingleFaceGroup = ({ faceGroupID }) => { - const { data, error, loading, fetchMore } = useQuery(SINGLE_FACE_GROUP, { - variables: { - limit: 200, - offset: 0, - id: faceGroupID, - }, - }) - const [presenting, setPresenting] = useState(false) - const [activeIndex, setActiveIndex] = useState(-1) - - const { containerElem, finished: finishedLoadingMore } = useScrollPagination({ - loading, - fetchMore, - data, - getItems: data => data.faceGroup.imageFaces, - }) - - const faceGroup = data?.faceGroup - - if (error) { - return
{error.message}
- } - - let mediaGallery = null - if (faceGroup) { - const media = faceGroup.imageFaces.map(x => x.media) - - const nextImage = () => - setActiveIndex(i => Math.min(i + 1, media.length - 1)) - - const previousImage = () => setActiveIndex(i => Math.max(i - 1, 0)) - - mediaGallery = ( -
- - -
- ) - } - - return ( -
- - {mediaGallery} -
- ) -} - -SingleFaceGroup.propTypes = { - faceGroupID: PropTypes.string, -} - -export default SingleFaceGroup From 287b0cc2044c91b7ede4bad1c05b3b9256d47862 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Wed, 19 May 2021 18:21:32 +0200 Subject: [PATCH 3/6] Refactor + more tests --- .../Pages/PeoplePage/FaceCircleImage.test.tsx | 40 +++ ui/src/Pages/PeoplePage/FaceCircleImage.tsx | 115 ++++++++ ui/src/Pages/PeoplePage/PeoplePage.test.tsx | 265 +++++++++++++----- ui/src/Pages/PeoplePage/PeoplePage.tsx | 2 +- .../SingleFaceGroup/SingleFaceGroup.tsx | 104 +++++++ 5 files changed, 452 insertions(+), 74 deletions(-) create mode 100644 ui/src/Pages/PeoplePage/FaceCircleImage.test.tsx create mode 100644 ui/src/Pages/PeoplePage/FaceCircleImage.tsx create mode 100644 ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.tsx diff --git a/ui/src/Pages/PeoplePage/FaceCircleImage.test.tsx b/ui/src/Pages/PeoplePage/FaceCircleImage.test.tsx new file mode 100644 index 00000000..ef9ced1e --- /dev/null +++ b/ui/src/Pages/PeoplePage/FaceCircleImage.test.tsx @@ -0,0 +1,40 @@ +import '@testing-library/jest-dom' + +import React from 'react' +import { render, screen } from '@testing-library/react' +import FaceCircleImage from './FaceCircleImage' +import { myFaces_myFaceGroups_imageFaces } from './__generated__/myFaces' + +require('../../localization').setupLocalization() + +test('face circle image', () => { + const imageFace: myFaces_myFaceGroups_imageFaces = { + __typename: 'ImageFace', + id: '3', + media: { + id: '1', + __typename: 'Media', + thumbnail: { + __typename: 'MediaURL', + url: 'http://localhost:4001/photo/thumbnail_my_image_jpg_p9x8dLWr.jpg', + width: 1024, + height: 641, + }, + }, + rectangle: { + __typename: 'FaceRectangle', + minX: 0.27, + maxX: 0.34, + minY: 0.76, + maxY: 0.88, + }, + } + + render() + + expect(screen.getByRole('img')).toBeInTheDocument() + expect(screen.getByRole('img')).toHaveAttribute( + 'src', + imageFace.media.thumbnail!.url + ) +}) diff --git a/ui/src/Pages/PeoplePage/FaceCircleImage.tsx b/ui/src/Pages/PeoplePage/FaceCircleImage.tsx new file mode 100644 index 00000000..d61d0575 --- /dev/null +++ b/ui/src/Pages/PeoplePage/FaceCircleImage.tsx @@ -0,0 +1,115 @@ +import React from 'react' +import styled from 'styled-components' +import { ProtectedImage } from '../../components/photoGallery/ProtectedMedia' +import { myFaces_myFaceGroups_imageFaces } from './__generated__/myFaces' + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const FaceImage = styled(({ origin, selectable, scale, ...rest }) => ( + +))<{ origin: { x: number; y: number }; selectable: boolean; scale: number }>` + position: absolute; + transform-origin: ${({ origin }) => `${origin.x * 100}% ${origin.y * 100}%`}; + object-fit: cover; + + transition: transform 250ms ease-out; +` + +const FaceImagePortrait = styled(FaceImage)` + width: 100%; + top: 50%; + transform: translateY(-50%) + ${({ origin, scale }) => + `translate(${(0.5 - origin.x) * 100}%, ${ + (0.5 - origin.y) * 100 + }%) scale(${Math.max(scale * 0.8, 1)})`}; + + ${({ selectable, origin, scale }) => + selectable + ? ` + &:hover { + transform: translateY(-50%) translate(${(0.5 - origin.x) * 100}%, ${ + (0.5 - origin.y) * 100 + }%) scale(${Math.max(scale * 0.85, 1)}) + ` + : ''} +` + +const FaceImageLandscape = styled(FaceImage)` + height: 100%; + left: 50%; + transform: translateX(-50%) + ${({ origin, scale }) => + `translate(${(0.5 - origin.x) * 100}%, ${ + (0.5 - origin.y) * 100 + }%) scale(${Math.max(scale * 0.8, 1)})`}; + + ${({ selectable, origin, scale }) => + selectable + ? ` + &:hover { + transform: translateX(-50%) translate(${(0.5 - origin.x) * 100}%, ${ + (0.5 - origin.y) * 100 + }%) scale(${Math.max(scale * 0.85, 1)}) + ` + : ''} +` + +const CircleImageWrapper = styled.div<{ size: string }>` + background-color: #eee; + position: relative; + border-radius: 50%; + width: ${({ size }) => size}; + height: ${({ size }) => size}; + object-fit: fill; + overflow: hidden; +` + +type FaceCircleImageProps = { + imageFace: myFaces_myFaceGroups_imageFaces + selectable: boolean + size?: string +} + +const FaceCircleImage = ({ + imageFace, + selectable, + size = '150px', +}: FaceCircleImageProps) => { + if (!imageFace) { + return null + } + + const rect = imageFace.rectangle + + const scale = Math.min( + 1 / (rect.maxX - rect.minX), + 1 / (rect.maxY - rect.minY) + ) + + const origin = { + x: (rect.minX + rect.maxX) / 2, + y: (rect.minY + rect.maxY) / 2, + } + + let SpecificFaceImage: typeof FaceImageLandscape | typeof FaceImagePortrait = + FaceImageLandscape + if (imageFace.media.thumbnail) { + SpecificFaceImage = + imageFace.media.thumbnail.width > imageFace.media.thumbnail.height + ? FaceImageLandscape + : FaceImagePortrait + } + + return ( + + + + ) +} + +export default FaceCircleImage diff --git a/ui/src/Pages/PeoplePage/PeoplePage.test.tsx b/ui/src/Pages/PeoplePage/PeoplePage.test.tsx index 0d9db98f..0df364ef 100644 --- a/ui/src/Pages/PeoplePage/PeoplePage.test.tsx +++ b/ui/src/Pages/PeoplePage/PeoplePage.test.tsx @@ -1,10 +1,15 @@ 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 { fireEvent, render, screen, waitFor } from '@testing-library/react' +import PeoplePage, { + FaceDetails, + MY_FACES_QUERY, + SET_GROUP_LABEL_MUTATION, +} from './PeoplePage' import { MockedProvider } from '@apollo/client/testing' import { MemoryRouter } from 'react-router' +import { myFaces_myFaceGroups } from './__generated__/myFaces' require('../../localization').setupLocalization() @@ -15,88 +20,202 @@ jest.mock('../../hooks/useScrollPagination', () => })) ) -const graphqlMocks = [ - { - request: { - query: MY_FACES_QUERY, - variables: { - limit: 50, - offset: 0, +describe('PeoplePage component', () => { + 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, + 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: [], - }, - ], + ], + }, + { + __typename: 'FaceGroup', + id: '1', + label: 'Person B', + imageFaceCount: 1, + imageFaces: [], + }, + ], + }, }, }, - }, -] + ] -test('people page', async () => { - const matchMock = { - params: { - person: undefined, - }, + 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() + }) +}) + +describe('FaceDetails component', () => { + const faceGroup: myFaces_myFaceGroups = { + id: '3', + label: null, + imageFaceCount: 2, + imageFaces: [ + { + id: '3', + rectangle: { + minX: 0.2705079913139343, + maxX: 0.3408200144767761, + minY: 0.7691109776496887, + maxY: 0.881434977054596, + __typename: 'FaceRectangle', + }, + media: { + id: '63', + thumbnail: { + url: 'http://localhost:4001/photo/thumbnail_image_jpg_p9x8dLWr.jpg', + width: 1024, + height: 641, + __typename: 'MediaURL', + }, + __typename: 'Media', + }, + __typename: 'ImageFace', + }, + ], + __typename: 'FaceGroup', } - render( - - - + test('unlabeled, no images', () => { + const emptyFaceGroup: myFaces_myFaceGroups = { + ...faceGroup, + imageFaces: [], + } + + 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.getByText('Unlabeled')).toBeInTheDocument() }) - expect( - screen.getAllByRole('link').some(x => x.getAttribute('href') == '/people/1') - ).toBeTruthy() + test('labeled, with thumbnail', () => { + const labeledFaceGroup: myFaces_myFaceGroups = { + ...faceGroup, + label: 'Some label', + } - expect( - screen.getAllByRole('link').some(x => x.getAttribute('href') == '/people/3') - ).toBeTruthy() + render( + + + + ) + + expect(screen.getByText(labeledFaceGroup.label!)).toBeInTheDocument() + expect(screen.queryByText('Unlabeled')).not.toBeInTheDocument() + }) + + test('add label to face group', async () => { + const graphqlMocks = [ + { + request: { + query: SET_GROUP_LABEL_MUTATION, + variables: { + groupID: '3', + label: 'John Doe', + }, + }, + newData: jest.fn(() => ({ + data: { + setFaceGroupLabel: { + __typename: 'FaceGroup', + id: '3', + label: 'John Doe', + }, + }, + })), + }, + ] + render( + + + + ) + + const btn = screen.getByRole('button') + expect(btn).toBeInTheDocument() + expect(screen.queryByRole('textbox')).not.toBeInTheDocument() + + fireEvent.click(btn) + + const input = screen.getByRole('textbox') + expect(input).toBeInTheDocument() + expect(input).toHaveValue('') + + fireEvent.change(input, { target: { value: 'John Doe' } }) + fireEvent.keyUp(input, { key: 'Enter', code: 'Enter' }) + + await waitFor(() => { + expect(graphqlMocks[0].newData).toHaveBeenCalled() + }) + }) }) diff --git a/ui/src/Pages/PeoplePage/PeoplePage.tsx b/ui/src/Pages/PeoplePage/PeoplePage.tsx index 2a625cd2..2aaf0fbd 100644 --- a/ui/src/Pages/PeoplePage/PeoplePage.tsx +++ b/ui/src/Pages/PeoplePage/PeoplePage.tsx @@ -86,7 +86,7 @@ type FaceDetailsProps = { group: myFaces_myFaceGroups } -const FaceDetails = ({ group }: FaceDetailsProps) => { +export const FaceDetails = ({ group }: FaceDetailsProps) => { const { t } = useTranslation() const [editLabel, setEditLabel] = useState(false) const [inputValue, setInputValue] = useState(group.label ?? '') diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.tsx b/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.tsx new file mode 100644 index 00000000..f72a8389 --- /dev/null +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.tsx @@ -0,0 +1,104 @@ +import { gql, useQuery } from '@apollo/client' +import React, { useEffect, useReducer } from 'react' +import PaginateLoader from '../../../components/PaginateLoader' +import PhotoGallery from '../../../components/photoGallery/PhotoGallery' +import { photoGalleryReducer } from '../../../components/photoGallery/photoGalleryReducer' +import useScrollPagination from '../../../hooks/useScrollPagination' +import FaceGroupTitle from './FaceGroupTitle' +import { + singleFaceGroup, + singleFaceGroupVariables, +} from './__generated__/singleFaceGroup' + +export const SINGLE_FACE_GROUP = gql` + query singleFaceGroup($id: ID!, $limit: Int!, $offset: Int!) { + faceGroup(id: $id) { + id + label + imageFaces(paginate: { limit: $limit, offset: $offset }) { + id + rectangle { + minX + maxX + minY + maxY + } + media { + id + type + title + thumbnail { + url + width + height + } + highRes { + url + } + favorite + } + } + } + } +` + +type SingleFaceGroupProps = { + faceGroupID: string +} + +const SingleFaceGroup = ({ faceGroupID }: SingleFaceGroupProps) => { + const { data, error, loading, fetchMore } = useQuery< + singleFaceGroup, + singleFaceGroupVariables + >(SINGLE_FACE_GROUP, { + variables: { + limit: 200, + offset: 0, + id: faceGroupID, + }, + }) + + const [mediaState, dispatchMedia] = useReducer(photoGalleryReducer, { + presenting: false, + activeIndex: -1, + media: [], + }) + + const { containerElem, finished: finishedLoadingMore } = + useScrollPagination({ + loading, + fetchMore, + data, + getItems: data => data.faceGroup.imageFaces, + }) + + useEffect(() => { + const media = data?.faceGroup.imageFaces.map(x => x.media) || [] + dispatchMedia({ type: 'replaceMedia', media }) + }, [data]) + + const faceGroup = data?.faceGroup + + if (error) { + return
{error.message}
+ } + + return ( +
+ +
+ + +
+
+ ) +} + +export default SingleFaceGroup From e2f6bdb365ac5aa22236b8252035e734473ae334 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Wed, 19 May 2021 20:56:53 +0200 Subject: [PATCH 4/6] Rewrite more faces related to Typescript --- .../Pages/PeoplePage/FaceCircleImage.test.tsx | 1 + ui/src/Pages/PeoplePage/FaceCircleImage.tsx | 14 +- ui/src/Pages/PeoplePage/PeoplePage.test.tsx | 4 +- ui/src/Pages/PeoplePage/PeoplePage.tsx | 1 + .../{FaceGroupTitle.js => FaceGroupTitle.tsx} | 38 +++--- ...eFacesModal.js => MoveImageFacesModal.tsx} | 64 ++++++--- ...GroupTable.js => SelectFaceGroupTable.tsx} | 55 +++++--- ...acesTable.js => SelectImageFacesTable.tsx} | 53 +++++--- .../Pages/PeoplePage/__generated__/myFaces.ts | 1 + .../__generated__/shareAlbumQuery.ts | 121 +++++++++--------- 10 files changed, 220 insertions(+), 132 deletions(-) rename ui/src/Pages/PeoplePage/SingleFaceGroup/{FaceGroupTitle.js => FaceGroupTitle.tsx} (83%) rename ui/src/Pages/PeoplePage/SingleFaceGroup/{MoveImageFacesModal.js => MoveImageFacesModal.tsx} (70%) rename ui/src/Pages/PeoplePage/SingleFaceGroup/{SelectFaceGroupTable.js => SelectFaceGroupTable.tsx} (68%) rename ui/src/Pages/PeoplePage/SingleFaceGroup/{SelectImageFacesTable.js => SelectImageFacesTable.tsx} (72%) diff --git a/ui/src/Pages/PeoplePage/FaceCircleImage.test.tsx b/ui/src/Pages/PeoplePage/FaceCircleImage.test.tsx index ef9ced1e..d82ffc17 100644 --- a/ui/src/Pages/PeoplePage/FaceCircleImage.test.tsx +++ b/ui/src/Pages/PeoplePage/FaceCircleImage.test.tsx @@ -14,6 +14,7 @@ test('face circle image', () => { media: { id: '1', __typename: 'Media', + title: 'my_image.jpg', thumbnail: { __typename: 'MediaURL', url: 'http://localhost:4001/photo/thumbnail_my_image_jpg_p9x8dLWr.jpg', diff --git a/ui/src/Pages/PeoplePage/FaceCircleImage.tsx b/ui/src/Pages/PeoplePage/FaceCircleImage.tsx index d61d0575..8fc4b774 100644 --- a/ui/src/Pages/PeoplePage/FaceCircleImage.tsx +++ b/ui/src/Pages/PeoplePage/FaceCircleImage.tsx @@ -1,7 +1,10 @@ import React from 'react' import styled from 'styled-components' import { ProtectedImage } from '../../components/photoGallery/ProtectedMedia' -import { myFaces_myFaceGroups_imageFaces } from './__generated__/myFaces' +import { + myFaces_myFaceGroups_imageFaces_media, + myFaces_myFaceGroups_imageFaces_rectangle, +} from './__generated__/myFaces' // eslint-disable-next-line @typescript-eslint/no-unused-vars const FaceImage = styled(({ origin, selectable, scale, ...rest }) => ( @@ -64,8 +67,15 @@ const CircleImageWrapper = styled.div<{ size: string }>` overflow: hidden; ` +type FaceCircleImageFace = { + __typename: 'ImageFace' + id: string + rectangle: myFaces_myFaceGroups_imageFaces_rectangle + media: myFaces_myFaceGroups_imageFaces_media +} + type FaceCircleImageProps = { - imageFace: myFaces_myFaceGroups_imageFaces + imageFace: FaceCircleImageFace selectable: boolean size?: string } diff --git a/ui/src/Pages/PeoplePage/PeoplePage.test.tsx b/ui/src/Pages/PeoplePage/PeoplePage.test.tsx index 0df364ef..9cbb819a 100644 --- a/ui/src/Pages/PeoplePage/PeoplePage.test.tsx +++ b/ui/src/Pages/PeoplePage/PeoplePage.test.tsx @@ -52,9 +52,10 @@ describe('PeoplePage component', () => { media: { __typename: 'Media', id: '63', + title: 'image.jpg', thumbnail: { __typename: 'MediaURL', - url: 'http://localhost:4001/photo/thumbnail_L%C3%B8berute_jpg_p9x8dLWr.jpg', + url: 'http://localhost:4001/photo/thumbnail_image_jpg_p9x8dLWr.jpg', width: 1024, height: 641, }, @@ -129,6 +130,7 @@ describe('FaceDetails component', () => { }, media: { id: '63', + title: 'image.jpg', thumbnail: { url: 'http://localhost:4001/photo/thumbnail_image_jpg_p9x8dLWr.jpg', width: 1024, diff --git a/ui/src/Pages/PeoplePage/PeoplePage.tsx b/ui/src/Pages/PeoplePage/PeoplePage.tsx index 2aaf0fbd..0698b651 100644 --- a/ui/src/Pages/PeoplePage/PeoplePage.tsx +++ b/ui/src/Pages/PeoplePage/PeoplePage.tsx @@ -36,6 +36,7 @@ export const MY_FACES_QUERY = gql` } media { id + title thumbnail { url width diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/FaceGroupTitle.js b/ui/src/Pages/PeoplePage/SingleFaceGroup/FaceGroupTitle.tsx similarity index 83% rename from ui/src/Pages/PeoplePage/SingleFaceGroup/FaceGroupTitle.js rename to ui/src/Pages/PeoplePage/SingleFaceGroup/FaceGroupTitle.tsx index ec30be59..69795787 100644 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/FaceGroupTitle.js +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/FaceGroupTitle.tsx @@ -1,18 +1,23 @@ import { useMutation } from '@apollo/client' -import PropTypes from 'prop-types' import React, { useState, useEffect, createRef } from 'react' import { Dropdown, Input } from 'semantic-ui-react' import styled from 'styled-components' +import { isNil } from '../../../helpers/utils' import { SET_GROUP_LABEL_MUTATION } from '../PeoplePage' +import { + setGroupLabel, + setGroupLabelVariables, +} from '../__generated__/setGroupLabel' import DetachImageFacesModal from './DetachImageFacesModal' import MergeFaceGroupsModal from './MergeFaceGroupsModal' import MoveImageFacesModal from './MoveImageFacesModal' +import { singleFaceGroup_faceGroup } from './__generated__/singleFaceGroup' const TitleWrapper = styled.div` min-height: 3.5em; ` -const TitleLabel = styled.h1` +const TitleLabel = styled.h1<{ labeled: boolean }>` display: inline-block; color: ${({ labeled }) => (labeled ? 'black' : '#888')}; margin-right: 12px; @@ -28,22 +33,22 @@ const TitleDropdown = styled(Dropdown)` } ` -const FaceGroupTitle = ({ faceGroup }) => { +type FaceGroupTitleProps = { + faceGroup?: singleFaceGroup_faceGroup +} + +const FaceGroupTitle = ({ faceGroup }: FaceGroupTitleProps) => { const [editLabel, setEditLabel] = useState(false) const [inputValue, setInputValue] = useState(faceGroup?.label ?? '') - const inputRef = createRef() + const inputRef = createRef() const [mergeModalOpen, setMergeModalOpen] = useState(false) const [moveModalOpen, setMoveModalOpen] = useState(false) const [detachModalOpen, setDetachModalOpen] = useState(false) - const [setGroupLabel, { loading: setLabelLoading }] = useMutation( - SET_GROUP_LABEL_MUTATION, - { - variables: { - groupID: faceGroup?.id, - }, - } - ) + const [setGroupLabel, { loading: setLabelLoading }] = useMutation< + setGroupLabel, + setGroupLabelVariables + >(SET_GROUP_LABEL_MUTATION) const resetLabel = () => { setInputValue(faceGroup?.label ?? '') @@ -62,7 +67,9 @@ const FaceGroupTitle = ({ faceGroup }) => { } }, [setLabelLoading]) - const onKeyUp = e => { + const onKeyUp = (e: KeyboardEvent & React.ChangeEvent) => { + if (isNil(faceGroup)) throw new Error('Expected faceGroup to be defined') + if (e.key == 'Escape') { resetLabel() return @@ -71,6 +78,7 @@ const FaceGroupTitle = ({ faceGroup }) => { if (e.key == 'Enter') { setGroupLabel({ variables: { + groupID: faceGroup.id, label: e.target.value == '' ? null : e.target.value, }, }) @@ -157,8 +165,4 @@ const FaceGroupTitle = ({ faceGroup }) => { ) } -FaceGroupTitle.propTypes = { - faceGroup: PropTypes.object, -} - export default FaceGroupTitle diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/MoveImageFacesModal.js b/ui/src/Pages/PeoplePage/SingleFaceGroup/MoveImageFacesModal.tsx similarity index 70% rename from ui/src/Pages/PeoplePage/SingleFaceGroup/MoveImageFacesModal.js rename to ui/src/Pages/PeoplePage/SingleFaceGroup/MoveImageFacesModal.tsx index 049745fd..226245df 100644 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/MoveImageFacesModal.js +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/MoveImageFacesModal.tsx @@ -1,11 +1,25 @@ import { gql, useLazyQuery, useMutation } from '@apollo/client' -import PropTypes from 'prop-types' import React, { useEffect, useState } from 'react' import { useHistory } from 'react-router-dom' import { Button, Modal } from 'semantic-ui-react' import SelectFaceGroupTable from './SelectFaceGroupTable' import SelectImageFacesTable from './SelectImageFacesTable' import { MY_FACES_QUERY } from '../PeoplePage' +import { + singleFaceGroup_faceGroup, + singleFaceGroup_faceGroup_imageFaces, +} from './__generated__/singleFaceGroup' +import { + myFaces, + myFacesVariables, + myFaces_myFaceGroups, + myFaces_myFaceGroups_imageFaces, +} from '../__generated__/myFaces' +import { isNil } from '../../../helpers/utils' +import { + moveImageFaces, + moveImageFacesVariables, +} from './__generated__/moveImageFaces' const MOVE_IMAGE_FACES_MUTATION = gql` mutation moveImageFaces($faceIDs: [ID!]!, $destFaceGroupID: ID!) { @@ -21,14 +35,29 @@ const MOVE_IMAGE_FACES_MUTATION = gql` } ` -const MoveImageFacesModal = ({ open, setOpen, faceGroup }) => { - const [selectedImageFaces, setSelectedImageFaces] = useState([]) - const [selectedFaceGroup, setSelectedFaceGroup] = useState(null) - const [imagesSelected, setImagesSelected] = useState(false) - let history = useHistory() +type MoveImageFacesModalProps = { + open: boolean + setOpen: React.Dispatch> + faceGroup?: singleFaceGroup_faceGroup +} - const [moveImageFacesMutation] = useMutation(MOVE_IMAGE_FACES_MUTATION, { - variables: {}, +const MoveImageFacesModal = ({ + open, + setOpen, + faceGroup, +}: MoveImageFacesModalProps) => { + const [selectedImageFaces, setSelectedImageFaces] = useState< + (singleFaceGroup_faceGroup_imageFaces | myFaces_myFaceGroups_imageFaces)[] + >([]) + const [selectedFaceGroup, setSelectedFaceGroup] = + useState(null) + const [imagesSelected, setImagesSelected] = useState(false) + const history = useHistory() + + const [moveImageFacesMutation] = useMutation< + moveImageFaces, + moveImageFacesVariables + >(MOVE_IMAGE_FACES_MUTATION, { refetchQueries: [ { query: MY_FACES_QUERY, @@ -36,9 +65,8 @@ const MoveImageFacesModal = ({ open, setOpen, faceGroup }) => { ], }) - const [loadFaceGroups, { data: faceGroupsData }] = useLazyQuery( - MY_FACES_QUERY - ) + const [loadFaceGroups, { data: faceGroupsData }] = + useLazyQuery(MY_FACES_QUERY) useEffect(() => { if (imagesSelected) { @@ -59,6 +87,10 @@ const MoveImageFacesModal = ({ open, setOpen, faceGroup }) => { const moveImageFaces = () => { const faceIDs = selectedImageFaces.map(face => face.id) + if (isNil(selectedFaceGroup)) { + throw new Error('Expected selectedFaceGroup not to be null') + } + moveImageFacesMutation({ variables: { faceIDs, @@ -83,9 +115,9 @@ const MoveImageFacesModal = ({ open, setOpen, faceGroup }) => { /> ) } else { - if (faceGroupsData) { + if (faceGroupsData && faceGroup) { const filteredFaceGroups = faceGroupsData.myFaceGroups.filter( - x => x != faceGroup + x => x.id != faceGroup.id ) table = ( { ) } -MoveImageFacesModal.propTypes = { - open: PropTypes.bool.isRequired, - setOpen: PropTypes.func.isRequired, - faceGroup: PropTypes.object, -} - export default MoveImageFacesModal diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.js b/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.tsx similarity index 68% rename from ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.js rename to ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.tsx index 13e869d0..7dd411ba 100644 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.js +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.tsx @@ -1,10 +1,11 @@ -import PropTypes from 'prop-types' import React, { useState, useEffect } from 'react' import { Input, Pagination, Table } from 'semantic-ui-react' import styled from 'styled-components' import FaceCircleImage from '../FaceCircleImage' +import { myFaces_myFaceGroups } from '../__generated__/myFaces' +import { singleFaceGroup_faceGroup } from './__generated__/singleFaceGroup' -const FaceCircleWrapper = styled.div` +const FaceCircleWrapper = styled.div<{ $selected: boolean }>` display: inline-block; border-radius: 50%; border: 2px solid @@ -16,17 +17,31 @@ const FlexCell = styled(Table.Cell)` align-items: center; ` -export const RowLabel = styled.span` +export const RowLabel = styled.span<{ $selected: boolean }>` ${({ $selected }) => $selected && `font-weight: bold;`} margin-left: 12px; ` -const FaceGroupRow = ({ faceGroup, faceSelected, setFaceSelected }) => { +type FaceGroupRowProps = { + faceGroup: myFaces_myFaceGroups + faceSelected: boolean + setFaceSelected(): void +} + +const FaceGroupRow = ({ + faceGroup, + faceSelected, + setFaceSelected, +}: FaceGroupRowProps) => { return ( - + {faceGroup.label} @@ -34,10 +49,15 @@ const FaceGroupRow = ({ faceGroup, faceSelected, setFaceSelected }) => { ) } -FaceGroupRow.propTypes = { - faceGroup: PropTypes.object.isRequired, - faceSelected: PropTypes.bool.isRequired, - setFaceSelected: PropTypes.func.isRequired, +type SelectFaceGroupTableProps = { + faceGroups: myFaces_myFaceGroups[] + selectedFaceGroup: singleFaceGroup_faceGroup | myFaces_myFaceGroups | null + setSelectedFaceGroup: React.Dispatch< + React.SetStateAction< + singleFaceGroup_faceGroup | myFaces_myFaceGroups | null + > + > + title: string } const SelectFaceGroupTable = ({ @@ -45,7 +65,7 @@ const SelectFaceGroupTable = ({ selectedFaceGroup, setSelectedFaceGroup, title, -}) => { +}: SelectFaceGroupTableProps) => { const PAGE_SIZE = 6 const [page, setPage] = useState(0) @@ -65,7 +85,7 @@ const SelectFaceGroupTable = ({ setSelectedFaceGroup(face)} /> )) @@ -105,7 +125,11 @@ const SelectFaceGroupTable = ({ activePage={page + 1} totalPages={Math.ceil(rows.length / PAGE_SIZE)} onPageChange={(_, { activePage }) => { - setPage(Math.ceil(activePage) - 1) + if (activePage) { + setPage(Math.ceil(activePage as number) - 1) + } else { + setPage(0) + } }} /> @@ -115,11 +139,4 @@ const SelectFaceGroupTable = ({ ) } -SelectFaceGroupTable.propTypes = { - faceGroups: PropTypes.array, - selectedFaceGroup: PropTypes.object, - setSelectedFaceGroup: PropTypes.func.isRequired, - title: PropTypes.string, -} - export default SelectFaceGroupTable diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.js b/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.tsx similarity index 72% rename from ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.js rename to ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.tsx index 4f476302..ea618be7 100644 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.js +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.tsx @@ -1,16 +1,29 @@ -import PropTypes from 'prop-types' import React, { useEffect, useState } from 'react' import { Checkbox, Input, Pagination, Table } from 'semantic-ui-react' import styled from 'styled-components' import { ProtectedImage } from '../../../components/photoGallery/ProtectedMedia' +import { myFaces_myFaceGroups_imageFaces } from '../__generated__/myFaces' import { RowLabel } from './SelectFaceGroupTable' +import { singleFaceGroup_faceGroup_imageFaces } from './__generated__/singleFaceGroup' const SelectImagePreview = styled(ProtectedImage)` max-width: 120px; max-height: 80px; ` -const ImageFaceRow = ({ imageFace, faceSelected, setFaceSelected }) => { +type ImageFaceRowProps = { + imageFace: + | myFaces_myFaceGroups_imageFaces + | singleFaceGroup_faceGroup_imageFaces + faceSelected: boolean + setFaceSelected(): void +} + +const ImageFaceRow = ({ + imageFace, + faceSelected, + setFaceSelected, +}: ImageFaceRowProps) => { return ( @@ -18,7 +31,7 @@ const ImageFaceRow = ({ imageFace, faceSelected, setFaceSelected }) => { @@ -31,10 +44,21 @@ const ImageFaceRow = ({ imageFace, faceSelected, setFaceSelected }) => { ) } -ImageFaceRow.propTypes = { - imageFace: PropTypes.object.isRequired, - faceSelected: PropTypes.bool.isRequired, - setFaceSelected: PropTypes.func.isRequired, +type SelectImageFacesTable = { + imageFaces: ( + | myFaces_myFaceGroups_imageFaces + | singleFaceGroup_faceGroup_imageFaces + )[] + selectedImageFaces: ( + | myFaces_myFaceGroups_imageFaces + | singleFaceGroup_faceGroup_imageFaces + )[] + setSelectedImageFaces: React.Dispatch< + React.SetStateAction< + (myFaces_myFaceGroups_imageFaces | singleFaceGroup_faceGroup_imageFaces)[] + > + > + title: string } const SelectImageFacesTable = ({ @@ -42,7 +66,7 @@ const SelectImageFacesTable = ({ selectedImageFaces, setSelectedImageFaces, title, -}) => { +}: SelectImageFacesTable) => { const PAGE_SIZE = 6 const [page, setPage] = useState(0) @@ -110,7 +134,11 @@ const SelectImageFacesTable = ({ activePage={page + 1} totalPages={Math.ceil(rows.length / PAGE_SIZE)} onPageChange={(_, { activePage }) => { - setPage(Math.ceil(activePage) - 1) + if (activePage) { + setPage(Math.ceil(activePage as number) - 1) + } else { + setPage(0) + } }} /> @@ -120,11 +148,4 @@ const SelectImageFacesTable = ({ ) } -SelectImageFacesTable.propTypes = { - imageFaces: PropTypes.array, - selectedImageFaces: PropTypes.array, - setSelectedImageFaces: PropTypes.func.isRequired, - title: PropTypes.string, -} - export default SelectImageFacesTable diff --git a/ui/src/Pages/PeoplePage/__generated__/myFaces.ts b/ui/src/Pages/PeoplePage/__generated__/myFaces.ts index b56b4ac7..b947e437 100644 --- a/ui/src/Pages/PeoplePage/__generated__/myFaces.ts +++ b/ui/src/Pages/PeoplePage/__generated__/myFaces.ts @@ -34,6 +34,7 @@ export interface myFaces_myFaceGroups_imageFaces_media_thumbnail { export interface myFaces_myFaceGroups_imageFaces_media { __typename: 'Media' id: string + title: string /** * URL to display the media in a smaller resolution */ diff --git a/ui/src/Pages/SharePage/__generated__/shareAlbumQuery.ts b/ui/src/Pages/SharePage/__generated__/shareAlbumQuery.ts index 31c6b5ef..c3186684 100644 --- a/ui/src/Pages/SharePage/__generated__/shareAlbumQuery.ts +++ b/ui/src/Pages/SharePage/__generated__/shareAlbumQuery.ts @@ -3,178 +3,181 @@ // @generated // This file was automatically generated and should not be edited. -import { MediaType } from "./../../../../__generated__/globalTypes"; +import { + OrderDirection, + MediaType, +} from './../../../../__generated__/globalTypes' // ==================================================== // GraphQL query operation: shareAlbumQuery // ==================================================== export interface shareAlbumQuery_album_subAlbums_thumbnail_thumbnail { - __typename: "MediaURL"; + __typename: 'MediaURL' /** * URL for previewing the image */ - url: string; + url: string } export interface shareAlbumQuery_album_subAlbums_thumbnail { - __typename: "Media"; + __typename: 'Media' /** * URL to display the media in a smaller resolution */ - thumbnail: shareAlbumQuery_album_subAlbums_thumbnail_thumbnail | null; + thumbnail: shareAlbumQuery_album_subAlbums_thumbnail_thumbnail | null } export interface shareAlbumQuery_album_subAlbums { - __typename: "Album"; - id: string; - title: string; + __typename: 'Album' + id: string + title: string /** * An image in this album used for previewing this album */ - thumbnail: shareAlbumQuery_album_subAlbums_thumbnail | null; + thumbnail: shareAlbumQuery_album_subAlbums_thumbnail | null } export interface shareAlbumQuery_album_media_thumbnail { - __typename: "MediaURL"; + __typename: 'MediaURL' /** * URL for previewing the image */ - url: string; + url: string /** * Width of the image in pixels */ - width: number; + width: number /** * Height of the image in pixels */ - height: number; + height: number } export interface shareAlbumQuery_album_media_downloads_mediaUrl { - __typename: "MediaURL"; + __typename: 'MediaURL' /** * URL for previewing the image */ - url: string; + url: string /** * Width of the image in pixels */ - width: number; + width: number /** * Height of the image in pixels */ - height: number; + height: number /** * The file size of the resource in bytes */ - fileSize: number; + fileSize: number } export interface shareAlbumQuery_album_media_downloads { - __typename: "MediaDownload"; - title: string; - mediaUrl: shareAlbumQuery_album_media_downloads_mediaUrl; + __typename: 'MediaDownload' + title: string + mediaUrl: shareAlbumQuery_album_media_downloads_mediaUrl } export interface shareAlbumQuery_album_media_highRes { - __typename: "MediaURL"; + __typename: 'MediaURL' /** * URL for previewing the image */ - url: string; + url: string /** * Width of the image in pixels */ - width: number; + width: number /** * Height of the image in pixels */ - height: number; + height: number } export interface shareAlbumQuery_album_media_videoWeb { - __typename: "MediaURL"; + __typename: 'MediaURL' /** * URL for previewing the image */ - url: string; + url: string } export interface shareAlbumQuery_album_media_exif { - __typename: "MediaEXIF"; + __typename: 'MediaEXIF' /** * The model name of the camera */ - camera: string | null; + camera: string | null /** * The maker of the camera */ - maker: string | null; + maker: string | null /** * The name of the lens */ - lens: string | null; - dateShot: any | null; + lens: string | null + dateShot: any | null /** * The exposure time of the image */ - exposure: number | null; + exposure: number | null /** * The aperature stops of the image */ - aperture: number | null; + aperture: number | null /** * The ISO setting of the image */ - iso: number | null; + iso: number | null /** * The focal length of the lens, when the image was taken */ - focalLength: number | null; + focalLength: number | null /** * A formatted description of the flash settings, when the image was taken */ - flash: number | null; + flash: number | null /** * An index describing the mode for adjusting the exposure of the image */ - exposureProgram: number | null; + exposureProgram: number | null } export interface shareAlbumQuery_album_media { - __typename: "Media"; - id: string; - title: string; - type: MediaType; + __typename: 'Media' + id: string + title: string + type: MediaType /** * URL to display the media in a smaller resolution */ - thumbnail: shareAlbumQuery_album_media_thumbnail | null; - downloads: shareAlbumQuery_album_media_downloads[]; + thumbnail: shareAlbumQuery_album_media_thumbnail | null + downloads: shareAlbumQuery_album_media_downloads[] /** * URL to display the photo in full resolution, will be null for videos */ - highRes: shareAlbumQuery_album_media_highRes | null; + highRes: shareAlbumQuery_album_media_highRes | null /** * URL to get the video in a web format that can be played in the browser, will be null for photos */ - videoWeb: shareAlbumQuery_album_media_videoWeb | null; - exif: shareAlbumQuery_album_media_exif | null; + videoWeb: shareAlbumQuery_album_media_videoWeb | null + exif: shareAlbumQuery_album_media_exif | null } export interface shareAlbumQuery_album { - __typename: "Album"; - id: string; - title: string; + __typename: 'Album' + id: string + title: string /** * The albums contained in this album */ - subAlbums: shareAlbumQuery_album_subAlbums[]; + subAlbums: shareAlbumQuery_album_subAlbums[] /** * The media inside this album */ - media: shareAlbumQuery_album_media[]; + media: shareAlbumQuery_album_media[] } export interface shareAlbumQuery { @@ -182,13 +185,15 @@ export interface shareAlbumQuery { * Get album by id, user must own the album or be admin * If valid tokenCredentials are provided, the album may be retrived without further authentication */ - album: shareAlbumQuery_album; + album: shareAlbumQuery_album } export interface shareAlbumQueryVariables { - id: string; - token: string; - password?: string | null; - limit?: number | null; - offset?: number | null; + id: string + token: string + password?: string | null + mediaOrderBy?: string | null + mediaOrderDirection?: OrderDirection | null + limit?: number | null + offset?: number | null } From 360db25ec32da84ddc45e6e0b1c82bd3961571dc Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Wed, 19 May 2021 21:32:49 +0200 Subject: [PATCH 5/6] Make people page translatable --- ui/extractedTranslations/da/translation.json | 52 +++++++- ui/extractedTranslations/de/translation.json | 52 +++++++- ui/extractedTranslations/en/translation.json | 52 +++++++- ui/extractedTranslations/es/translation.json | 52 +++++++- ui/extractedTranslations/fr/translation.json | 52 +++++++- ui/extractedTranslations/it/translation.json | 52 +++++++- ui/extractedTranslations/pl/translation.json | 52 +++++++- ui/extractedTranslations/sv/translation.json | 52 +++++++- ...acesModal.js => DetachImageFacesModal.tsx} | 68 +++++++--- .../SingleFaceGroup/FaceGroupTitle.tsx | 65 ++++++--- .../SingleFaceGroup/MergeFaceGroupsModal.js | 94 ------------- .../SingleFaceGroup/MergeFaceGroupsModal.tsx | 124 ++++++++++++++++++ .../SingleFaceGroup/MoveImageFacesModal.tsx | 44 +++++-- .../SingleFaceGroup/SelectFaceGroupTable.tsx | 8 +- .../SingleFaceGroup/SelectImageFacesTable.tsx | 8 +- .../SingleFaceGroup/SingleFaceGroup.tsx | 5 +- 16 files changed, 671 insertions(+), 161 deletions(-) rename ui/src/Pages/PeoplePage/SingleFaceGroup/{DetachImageFacesModal.js => DetachImageFacesModal.tsx} (52%) delete mode 100644 ui/src/Pages/PeoplePage/SingleFaceGroup/MergeFaceGroupsModal.js create mode 100644 ui/src/Pages/PeoplePage/SingleFaceGroup/MergeFaceGroupsModal.tsx diff --git a/ui/extractedTranslations/da/translation.json b/ui/extractedTranslations/da/translation.json index b30c1010..c4c49d4a 100644 --- a/ui/extractedTranslations/da/translation.json +++ b/ui/extractedTranslations/da/translation.json @@ -68,10 +68,58 @@ }, "people_page": { "face_group": { + "action": { + "add_label": null, + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, "label_placeholder": "Navn", - "unlabeled": "Ikke navngivet" + "unlabeled": "Ikke navngivet", + "unlabeled_person": null }, - "recognize_unlabeled_faces_button": "Genkend ikke navngivede ansigter" + "modal": { + "action": { + "merge": null + }, + "detach_image_faces": { + "action": { + "detach": null, + "select_images": null + }, + "description": null, + "title": null + }, + "merge_face_groups": { + "description": null, + "destination_table": { + "title": null + }, + "title": null + }, + "move_image_faces": { + "description": null, + "destination_face_group_table": { + "move_action": null, + "title": null + }, + "image_select_table": { + "next_action": null, + "title": null + }, + "title": null + } + }, + "recognize_unlabeled_faces_button": "Genkend ikke navngivede ansigter", + "table": { + "select_face_group": { + "search_faces_placeholder": null + }, + "select_image_faces": { + "search_images_placeholder": null + } + } }, "photos_page": { "title": "Billeder" diff --git a/ui/extractedTranslations/de/translation.json b/ui/extractedTranslations/de/translation.json index 558a37b6..542e988e 100644 --- a/ui/extractedTranslations/de/translation.json +++ b/ui/extractedTranslations/de/translation.json @@ -68,10 +68,58 @@ }, "people_page": { "face_group": { + "action": { + "add_label": null, + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, "label_placeholder": "Zuordnung", - "unlabeled": "Nicht zugeordnet" + "unlabeled": "Nicht zugeordnet", + "unlabeled_person": null }, - "recognize_unlabeled_faces_button": "Nicht zugeordnete Gesichter erkennen" + "modal": { + "action": { + "merge": null + }, + "detach_image_faces": { + "action": { + "detach": null, + "select_images": null + }, + "description": null, + "title": null + }, + "merge_face_groups": { + "description": null, + "destination_table": { + "title": null + }, + "title": null + }, + "move_image_faces": { + "description": null, + "destination_face_group_table": { + "move_action": null, + "title": null + }, + "image_select_table": { + "next_action": null, + "title": null + }, + "title": null + } + }, + "recognize_unlabeled_faces_button": "Nicht zugeordnete Gesichter erkennen", + "table": { + "select_face_group": { + "search_faces_placeholder": null + }, + "select_image_faces": { + "search_images_placeholder": null + } + } }, "photos_page": { "title": "Fotos" diff --git a/ui/extractedTranslations/en/translation.json b/ui/extractedTranslations/en/translation.json index 3d9820c0..21881e01 100644 --- a/ui/extractedTranslations/en/translation.json +++ b/ui/extractedTranslations/en/translation.json @@ -68,10 +68,58 @@ }, "people_page": { "face_group": { + "action": { + "add_label": "Add Label", + "change_label": "Change Label", + "detach_face": "Detach Face", + "merge_face": "Merge Face", + "move_faces": "Move Faces" + }, "label_placeholder": "Label", - "unlabeled": "Unlabeled" + "unlabeled": "Unlabeled", + "unlabeled_person": "Unlabeled person" }, - "recognize_unlabeled_faces_button": "Recognize unlabeled faces" + "modal": { + "action": { + "merge": "Merge" + }, + "detach_image_faces": { + "action": { + "detach": "Detach image faces", + "select_images": "Select images to detach" + }, + "description": "Detach selected images of this face group and move them to a new face groups", + "title": "Detach Image Faces" + }, + "merge_face_groups": { + "description": "All images within this face group will be merged into the selected face group.", + "destination_table": { + "title": "Select the destination face" + }, + "title": "Merge Face Groups" + }, + "move_image_faces": { + "description": "Move selected images of this face group to another face group", + "destination_face_group_table": { + "move_action": "Move image faces", + "title": "Select destination face group" + }, + "image_select_table": { + "next_action": "Next", + "title": "Select images to move" + }, + "title": "Move Image Faces" + } + }, + "recognize_unlabeled_faces_button": "Recognize unlabeled faces", + "table": { + "select_face_group": { + "search_faces_placeholder": "Search faces..." + }, + "select_image_faces": { + "search_images_placeholder": "Search images..." + } + } }, "photos_page": { "title": "Photos" diff --git a/ui/extractedTranslations/es/translation.json b/ui/extractedTranslations/es/translation.json index b52e82a9..3be1421a 100644 --- a/ui/extractedTranslations/es/translation.json +++ b/ui/extractedTranslations/es/translation.json @@ -68,10 +68,58 @@ }, "people_page": { "face_group": { + "action": { + "add_label": null, + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, "label_placeholder": "Etiqueta", - "unlabeled": "Sin etiquetar" + "unlabeled": "Sin etiquetar", + "unlabeled_person": null }, - "recognize_unlabeled_faces_button": "Reconocer caras sin etiquetar" + "modal": { + "action": { + "merge": null + }, + "detach_image_faces": { + "action": { + "detach": null, + "select_images": null + }, + "description": null, + "title": null + }, + "merge_face_groups": { + "description": null, + "destination_table": { + "title": null + }, + "title": null + }, + "move_image_faces": { + "description": null, + "destination_face_group_table": { + "move_action": null, + "title": null + }, + "image_select_table": { + "next_action": null, + "title": null + }, + "title": null + } + }, + "recognize_unlabeled_faces_button": "Reconocer caras sin etiquetar", + "table": { + "select_face_group": { + "search_faces_placeholder": null + }, + "select_image_faces": { + "search_images_placeholder": null + } + } }, "photos_page": { "title": "Fotos" diff --git a/ui/extractedTranslations/fr/translation.json b/ui/extractedTranslations/fr/translation.json index 2078abeb..8a847c6b 100644 --- a/ui/extractedTranslations/fr/translation.json +++ b/ui/extractedTranslations/fr/translation.json @@ -68,10 +68,58 @@ }, "people_page": { "face_group": { + "action": { + "add_label": null, + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, "label_placeholder": "Étiquette", - "unlabeled": "Sans étiquette" + "unlabeled": "Sans étiquette", + "unlabeled_person": null }, - "recognize_unlabeled_faces_button": "Reconnaître les visages sans étiquette" + "modal": { + "action": { + "merge": null + }, + "detach_image_faces": { + "action": { + "detach": null, + "select_images": null + }, + "description": null, + "title": null + }, + "merge_face_groups": { + "description": null, + "destination_table": { + "title": null + }, + "title": null + }, + "move_image_faces": { + "description": null, + "destination_face_group_table": { + "move_action": null, + "title": null + }, + "image_select_table": { + "next_action": null, + "title": null + }, + "title": null + } + }, + "recognize_unlabeled_faces_button": "Reconnaître les visages sans étiquette", + "table": { + "select_face_group": { + "search_faces_placeholder": null + }, + "select_image_faces": { + "search_images_placeholder": null + } + } }, "photos_page": { "title": "Photos" diff --git a/ui/extractedTranslations/it/translation.json b/ui/extractedTranslations/it/translation.json index 34b3c62e..05cd7089 100644 --- a/ui/extractedTranslations/it/translation.json +++ b/ui/extractedTranslations/it/translation.json @@ -68,10 +68,58 @@ }, "people_page": { "face_group": { + "action": { + "add_label": null, + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, "label_placeholder": "Etichetta", - "unlabeled": "Senza etichetta" + "unlabeled": "Senza etichetta", + "unlabeled_person": null }, - "recognize_unlabeled_faces_button": "Identifica facce senza etichetta" + "modal": { + "action": { + "merge": null + }, + "detach_image_faces": { + "action": { + "detach": null, + "select_images": null + }, + "description": null, + "title": null + }, + "merge_face_groups": { + "description": null, + "destination_table": { + "title": null + }, + "title": null + }, + "move_image_faces": { + "description": null, + "destination_face_group_table": { + "move_action": null, + "title": null + }, + "image_select_table": { + "next_action": null, + "title": null + }, + "title": null + } + }, + "recognize_unlabeled_faces_button": "Identifica facce senza etichetta", + "table": { + "select_face_group": { + "search_faces_placeholder": null + }, + "select_image_faces": { + "search_images_placeholder": null + } + } }, "photos_page": { "title": "Foto" diff --git a/ui/extractedTranslations/pl/translation.json b/ui/extractedTranslations/pl/translation.json index 96d9d103..83ec26d1 100644 --- a/ui/extractedTranslations/pl/translation.json +++ b/ui/extractedTranslations/pl/translation.json @@ -68,10 +68,58 @@ }, "people_page": { "face_group": { + "action": { + "add_label": null, + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, "label_placeholder": "Etykieta", - "unlabeled": "Nieoznakowany" + "unlabeled": "Nieoznakowany", + "unlabeled_person": null }, - "recognize_unlabeled_faces_button": "Rozpoznaj nieoznakowane twarze" + "modal": { + "action": { + "merge": null + }, + "detach_image_faces": { + "action": { + "detach": null, + "select_images": null + }, + "description": null, + "title": null + }, + "merge_face_groups": { + "description": null, + "destination_table": { + "title": null + }, + "title": null + }, + "move_image_faces": { + "description": null, + "destination_face_group_table": { + "move_action": null, + "title": null + }, + "image_select_table": { + "next_action": null, + "title": null + }, + "title": null + } + }, + "recognize_unlabeled_faces_button": "Rozpoznaj nieoznakowane twarze", + "table": { + "select_face_group": { + "search_faces_placeholder": null + }, + "select_image_faces": { + "search_images_placeholder": null + } + } }, "photos_page": { "title": "Zdjęcia" diff --git a/ui/extractedTranslations/sv/translation.json b/ui/extractedTranslations/sv/translation.json index aab3904d..ce1a52c5 100644 --- a/ui/extractedTranslations/sv/translation.json +++ b/ui/extractedTranslations/sv/translation.json @@ -68,10 +68,58 @@ }, "people_page": { "face_group": { + "action": { + "add_label": null, + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, "label_placeholder": "Märkning", - "unlabeled": "Omärkt" + "unlabeled": "Omärkt", + "unlabeled_person": null }, - "recognize_unlabeled_faces_button": "Känna igen omärkta ansikten" + "modal": { + "action": { + "merge": null + }, + "detach_image_faces": { + "action": { + "detach": null, + "select_images": null + }, + "description": null, + "title": null + }, + "merge_face_groups": { + "description": null, + "destination_table": { + "title": null + }, + "title": null + }, + "move_image_faces": { + "description": null, + "destination_face_group_table": { + "move_action": null, + "title": null + }, + "image_select_table": { + "next_action": null, + "title": null + }, + "title": null + } + }, + "recognize_unlabeled_faces_button": "Känna igen omärkta ansikten", + "table": { + "select_face_group": { + "search_faces_placeholder": null + }, + "select_image_faces": { + "search_images_placeholder": null + } + } }, "photos_page": { "title": "Bilder" diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal.js b/ui/src/Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal.tsx similarity index 52% rename from ui/src/Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal.js rename to ui/src/Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal.tsx index 7a53d872..d7701315 100644 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal.js +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal.tsx @@ -1,10 +1,23 @@ import { gql, useMutation } from '@apollo/client' -import PropTypes from 'prop-types' import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import { useHistory } from 'react-router-dom' import { Button, Modal } from 'semantic-ui-react' +import { isNil } from '../../../helpers/utils' import { MY_FACES_QUERY } from '../PeoplePage' +import { + myFaces_myFaceGroups, + myFaces_myFaceGroups_imageFaces, +} from '../__generated__/myFaces' import SelectImageFacesTable from './SelectImageFacesTable' +import { + detachImageFaces, + detachImageFacesVariables, +} from './__generated__/detachImageFaces' +import { + singleFaceGroup_faceGroup, + singleFaceGroup_faceGroup_imageFaces, +} from './__generated__/singleFaceGroup' const DETACH_IMAGE_FACES_MUTATION = gql` mutation detachImageFaces($faceIDs: [ID!]!) { @@ -15,12 +28,28 @@ const DETACH_IMAGE_FACES_MUTATION = gql` } ` -const DetachImageFacesModal = ({ open, setOpen, faceGroup }) => { - const [selectedImageFaces, setSelectedImageFaces] = useState([]) - let history = useHistory() +type DetachImageFacesModalProps = { + open: boolean + setOpen(open: boolean): void + faceGroup: myFaces_myFaceGroups | singleFaceGroup_faceGroup +} - const [detachImageFacesMutation] = useMutation(DETACH_IMAGE_FACES_MUTATION, { - variables: {}, +const DetachImageFacesModal = ({ + open, + setOpen, + faceGroup, +}: DetachImageFacesModalProps) => { + const { t } = useTranslation() + + const [selectedImageFaces, setSelectedImageFaces] = useState< + (myFaces_myFaceGroups_imageFaces | singleFaceGroup_faceGroup_imageFaces)[] + >([]) + const history = useHistory() + + const [detachImageFacesMutation] = useMutation< + detachImageFaces, + detachImageFacesVariables + >(DETACH_IMAGE_FACES_MUTATION, { refetchQueries: [ { query: MY_FACES_QUERY, @@ -44,6 +73,7 @@ const DetachImageFacesModal = ({ open, setOpen, faceGroup }) => { faceIDs, }, }).then(({ data }) => { + if (isNil(data)) throw new Error('Expected data not to be null') setOpen(false) history.push(`/people/${data.detachImageFaces.id}`) }) @@ -57,18 +87,25 @@ const DetachImageFacesModal = ({ open, setOpen, faceGroup }) => { onOpen={() => setOpen(true)} open={open} > - Detach Image Faces + + {t('people_page.modal.detach_image_faces.title', 'Detach Image Faces')} +

- Detach selected images of this face group and move them to a new - face group + {t( + 'people_page.modal.detach_image_faces.description', + 'Detach selected images of this face group and move them to a new face groups' + )}

@@ -76,7 +113,10 @@ const DetachImageFacesModal = ({ open, setOpen, faceGroup }) => { - + + {positiveButton} diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.tsx b/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.tsx index 7dd411ba..d7ef5fb1 100644 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.tsx +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react' +import { useTranslation } from 'react-i18next' import { Input, Pagination, Table } from 'semantic-ui-react' import styled from 'styled-components' import FaceCircleImage from '../FaceCircleImage' @@ -66,6 +67,8 @@ const SelectFaceGroupTable = ({ setSelectedFaceGroup, title, }: SelectFaceGroupTableProps) => { + const { t } = useTranslation() + const PAGE_SIZE = 6 const [page, setPage] = useState(0) @@ -106,7 +109,10 @@ const SelectFaceGroupTable = ({ value={searchValue} onChange={e => setSearchValue(e.target.value)} icon="search" - placeholder="Search faces..." + placeholder={t( + 'people_page.table.select_face_group.search_faces_placeholder', + 'Search faces...' + )} fluid /> diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.tsx b/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.tsx index ea618be7..b33b057f 100644 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.tsx +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import { Checkbox, Input, Pagination, Table } from 'semantic-ui-react' import styled from 'styled-components' import { ProtectedImage } from '../../../components/photoGallery/ProtectedMedia' @@ -67,6 +68,8 @@ const SelectImageFacesTable = ({ setSelectedImageFaces, title, }: SelectImageFacesTable) => { + const { t } = useTranslation() + const PAGE_SIZE = 6 const [page, setPage] = useState(0) @@ -115,7 +118,10 @@ const SelectImageFacesTable = ({ value={searchValue} onChange={e => setSearchValue(e.target.value)} icon="search" - placeholder="Search images..." + placeholder={t( + 'people_page.table.select_image_faces.search_images_placeholder', + 'Search images...' + )} fluid /> diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.tsx b/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.tsx index f72a8389..73270402 100644 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.tsx +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.tsx @@ -1,5 +1,6 @@ import { gql, useQuery } from '@apollo/client' import React, { useEffect, useReducer } from 'react' +import { useTranslation } from 'react-i18next' import PaginateLoader from '../../../components/PaginateLoader' import PhotoGallery from '../../../components/photoGallery/PhotoGallery' import { photoGalleryReducer } from '../../../components/photoGallery/photoGalleryReducer' @@ -47,6 +48,8 @@ type SingleFaceGroupProps = { } const SingleFaceGroup = ({ faceGroupID }: SingleFaceGroupProps) => { + const { t } = useTranslation() + const { data, error, loading, fetchMore } = useQuery< singleFaceGroup, singleFaceGroupVariables @@ -94,7 +97,7 @@ const SingleFaceGroup = ({ faceGroupID }: SingleFaceGroupProps) => { /> From caf086784124850d23e12b89c6726a6ed882316c Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Wed, 19 May 2021 21:43:42 +0200 Subject: [PATCH 6/6] Update danish translation --- ui/extractedTranslations/da/translation.json | 52 ++++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/ui/extractedTranslations/da/translation.json b/ui/extractedTranslations/da/translation.json index c4c49d4a..bc6f3103 100644 --- a/ui/extractedTranslations/da/translation.json +++ b/ui/extractedTranslations/da/translation.json @@ -69,55 +69,55 @@ "people_page": { "face_group": { "action": { - "add_label": null, - "change_label": null, - "detach_face": null, - "merge_face": null, - "move_faces": null + "add_label": "Tilføj navn", + "change_label": "Ændre navn", + "detach_face": "Løsriv ansigter", + "merge_face": "Sammenflet ansigter", + "move_faces": "Flyt ansigter" }, "label_placeholder": "Navn", "unlabeled": "Ikke navngivet", - "unlabeled_person": null + "unlabeled_person": "Ikke navngivet person" }, "modal": { "action": { - "merge": null + "merge": "Sammenflet" }, "detach_image_faces": { "action": { - "detach": null, - "select_images": null + "detach": "Løsriv billeder", + "select_images": "Vælg billeder som skal løsrives" }, - "description": null, - "title": null + "description": "Løsriv valgte billeder fra denne gruppe og flyt dem til sin egen gruppe", + "title": "Løsriv Billeder" }, "merge_face_groups": { - "description": null, + "description": "Alle billeder fra denne gruppe vil blive flettet sammen med den valgte gruppe", "destination_table": { - "title": null + "title": "Vælg destinationsgruppe" }, - "title": null + "title": "Vælg gruppe at flette med" }, "move_image_faces": { - "description": null, + "description": "Flyt valgte billeder fra denne gruppe til en anden gruppe", "destination_face_group_table": { - "move_action": null, - "title": null + "move_action": "Flyt ansigter", + "title": "Vælg destinationsgruppe" }, "image_select_table": { - "next_action": null, - "title": null + "next_action": "Næste", + "title": "Vælg billeder som skal flyttes" }, - "title": null + "title": "Flyt Ansigter" } }, "recognize_unlabeled_faces_button": "Genkend ikke navngivede ansigter", "table": { "select_face_group": { - "search_faces_placeholder": null + "search_faces_placeholder": "Søg ansigter..." }, "select_image_faces": { - "search_images_placeholder": null + "search_images_placeholder": "Søg billeder..." } } }, @@ -204,9 +204,9 @@ "title": "Brugere" }, "version_info": { - "build_date_title": null, - "title": null, - "version_title": null + "build_date_title": "Dato for udgivelse", + "title": "Photoview Version", + "version_title": "Udgivelsesversion" } }, "share_page": { @@ -300,7 +300,7 @@ }, "title": { "loading_album": "Loader album", - "login": null, + "login": "Logind", "people": "Personer", "settings": "Indstillinger" }