Rewrite more faces related to Typescript

This commit is contained in:
viktorstrate
2021-05-19 20:56:53 +02:00
parent 287b0cc204
commit e2f6bdb365
10 changed files with 220 additions and 132 deletions

View File

@@ -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',

View File

@@ -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
}

View File

@@ -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,

View File

@@ -36,6 +36,7 @@ export const MY_FACES_QUERY = gql`
}
media {
id
title
thumbnail {
url
width

View File

@@ -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<Input>()
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<HTMLInputElement>) => {
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

View File

@@ -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<React.SetStateAction<boolean>>
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<myFaces_myFaceGroups | singleFaceGroup_faceGroup | null>(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<myFaces, myFacesVariables>(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 = (
<SelectFaceGroupTable
@@ -146,10 +178,4 @@ const MoveImageFacesModal = ({ open, setOpen, faceGroup }) => {
)
}
MoveImageFacesModal.propTypes = {
open: PropTypes.bool.isRequired,
setOpen: PropTypes.func.isRequired,
faceGroup: PropTypes.object,
}
export default MoveImageFacesModal

View File

@@ -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 (
<Table.Row key={faceGroup.id} onClick={setFaceSelected}>
<FlexCell>
<FaceCircleWrapper $selected={faceSelected}>
<FaceCircleImage imageFace={faceGroup.imageFaces[0]} size="50px" />
<FaceCircleImage
imageFace={faceGroup.imageFaces[0]}
size="50px"
selectable={false}
/>
</FaceCircleWrapper>
<RowLabel $selected={faceSelected}>{faceGroup.label}</RowLabel>
</FlexCell>
@@ -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 = ({
<FaceGroupRow
key={face.id}
faceGroup={face}
faceSelected={selectedFaceGroup == face}
faceSelected={selectedFaceGroup?.id == face.id}
setFaceSelected={() => 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)
}
}}
/>
</Table.HeaderCell>
@@ -115,11 +139,4 @@ const SelectFaceGroupTable = ({
)
}
SelectFaceGroupTable.propTypes = {
faceGroups: PropTypes.array,
selectedFaceGroup: PropTypes.object,
setSelectedFaceGroup: PropTypes.func.isRequired,
title: PropTypes.string,
}
export default SelectFaceGroupTable

View File

@@ -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 (
<Table.Row key={imageFace.id}>
<Table.Cell>
@@ -18,7 +31,7 @@ const ImageFaceRow = ({ imageFace, faceSelected, setFaceSelected }) => {
</Table.Cell>
<Table.Cell>
<SelectImagePreview
src={imageFace.media.thumbnail.url}
src={imageFace.media.thumbnail?.url}
onClick={setFaceSelected}
/>
</Table.Cell>
@@ -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)
}
}}
/>
</Table.HeaderCell>
@@ -120,11 +148,4 @@ const SelectImageFacesTable = ({
)
}
SelectImageFacesTable.propTypes = {
imageFaces: PropTypes.array,
selectedImageFaces: PropTypes.array,
setSelectedImageFaces: PropTypes.func.isRequired,
title: PropTypes.string,
}
export default SelectImageFacesTable

View File

@@ -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
*/

View File

@@ -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
}