diff --git a/api/graphql/generated.go b/api/graphql/generated.go index 03d85203..cdad8e4c 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -1715,29 +1715,29 @@ type Mutation { scanUser(userId: ID!): ScannerResult! @isAdmin "Generate share token for album" - shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken @isAuthorized + shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken! @isAuthorized "Generate share token for media" - shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken @isAuthorized + shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken! @isAuthorized "Delete a share token by it's token value" - deleteShareToken(token: String!): ShareToken @isAuthorized + deleteShareToken(token: String!): ShareToken! @isAuthorized "Set a password for a token, if null is passed for the password argument, the password will be cleared" - protectShareToken(token: String!, password: String): ShareToken @isAuthorized + protectShareToken(token: String!, password: String): ShareToken! @isAuthorized "Mark or unmark a media as being a favorite" - favoriteMedia(mediaId: ID!, favorite: Boolean!): Media @isAuthorized + favoriteMedia(mediaId: ID!, favorite: Boolean!): Media! @isAuthorized updateUser( id: ID! username: String password: String admin: Boolean - ): User @isAdmin + ): User! @isAdmin createUser( username: String! password: String admin: Boolean! - ): User @isAdmin - deleteUser(id: ID!): User @isAdmin + ): User! @isAdmin + deleteUser(id: ID!): User! @isAdmin "Add a root path from where to look for media for the given user" userAddRootPath(id: ID!, rootPath: String!): Album @isAdmin @@ -5084,11 +5084,14 @@ func (ec *executionContext) _Mutation_shareAlbum(ctx context.Context, field grap return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } res := resTmp.(*models.ShareToken) fc.Result = res - return ec.marshalOShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res) + return ec.marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res) } func (ec *executionContext) _Mutation_shareMedia(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -5143,11 +5146,14 @@ func (ec *executionContext) _Mutation_shareMedia(ctx context.Context, field grap return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } res := resTmp.(*models.ShareToken) fc.Result = res - return ec.marshalOShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res) + return ec.marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res) } func (ec *executionContext) _Mutation_deleteShareToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -5202,11 +5208,14 @@ func (ec *executionContext) _Mutation_deleteShareToken(ctx context.Context, fiel return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } res := resTmp.(*models.ShareToken) fc.Result = res - return ec.marshalOShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res) + return ec.marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res) } func (ec *executionContext) _Mutation_protectShareToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -5261,11 +5270,14 @@ func (ec *executionContext) _Mutation_protectShareToken(ctx context.Context, fie return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } res := resTmp.(*models.ShareToken) fc.Result = res - return ec.marshalOShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res) + return ec.marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res) } func (ec *executionContext) _Mutation_favoriteMedia(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -5320,11 +5332,14 @@ func (ec *executionContext) _Mutation_favoriteMedia(ctx context.Context, field g return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } res := resTmp.(*models.Media) fc.Result = res - return ec.marshalOMedia2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, field.Selections, res) + return ec.marshalNMedia2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, field.Selections, res) } func (ec *executionContext) _Mutation_updateUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -5379,11 +5394,14 @@ func (ec *executionContext) _Mutation_updateUser(ctx context.Context, field grap return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } res := resTmp.(*models.User) fc.Result = res - return ec.marshalOUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res) + return ec.marshalNUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res) } func (ec *executionContext) _Mutation_createUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -5438,11 +5456,14 @@ func (ec *executionContext) _Mutation_createUser(ctx context.Context, field grap return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } res := resTmp.(*models.User) fc.Result = res - return ec.marshalOUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res) + return ec.marshalNUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res) } func (ec *executionContext) _Mutation_deleteUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -5497,11 +5518,14 @@ func (ec *executionContext) _Mutation_deleteUser(ctx context.Context, field grap return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } res := resTmp.(*models.User) fc.Result = res - return ec.marshalOUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res) + return ec.marshalNUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res) } func (ec *executionContext) _Mutation_userAddRootPath(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -10520,20 +10544,44 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) } case "shareAlbum": out.Values[i] = ec._Mutation_shareAlbum(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "shareMedia": out.Values[i] = ec._Mutation_shareMedia(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "deleteShareToken": out.Values[i] = ec._Mutation_deleteShareToken(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "protectShareToken": out.Values[i] = ec._Mutation_protectShareToken(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "favoriteMedia": out.Values[i] = ec._Mutation_favoriteMedia(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "updateUser": out.Values[i] = ec._Mutation_updateUser(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "createUser": out.Values[i] = ec._Mutation_createUser(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "deleteUser": out.Values[i] = ec._Mutation_deleteUser(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "userAddRootPath": out.Values[i] = ec._Mutation_userAddRootPath(ctx, field) case "userRemoveRootAlbum": @@ -12685,13 +12733,6 @@ func (ec *executionContext) marshalOTime2ᚖtimeᚐTime(ctx context.Context, sel return graphql.MarshalTime(*v) } -func (ec *executionContext) marshalOUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx context.Context, sel ast.SelectionSet, v *models.User) graphql.Marshaler { - if v == nil { - return graphql.Null - } - return ec._User(ctx, sel, v) -} - func (ec *executionContext) marshalOVideoMetadata2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐVideoMetadata(ctx context.Context, sel ast.SelectionSet, v *models.VideoMetadata) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index bf818239..ab085d7b 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -95,29 +95,29 @@ type Mutation { scanUser(userId: ID!): ScannerResult! @isAdmin "Generate share token for album" - shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken @isAuthorized + shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken! @isAuthorized "Generate share token for media" - shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken @isAuthorized + shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken! @isAuthorized "Delete a share token by it's token value" - deleteShareToken(token: String!): ShareToken @isAuthorized + deleteShareToken(token: String!): ShareToken! @isAuthorized "Set a password for a token, if null is passed for the password argument, the password will be cleared" - protectShareToken(token: String!, password: String): ShareToken @isAuthorized + protectShareToken(token: String!, password: String): ShareToken! @isAuthorized "Mark or unmark a media as being a favorite" - favoriteMedia(mediaId: ID!, favorite: Boolean!): Media @isAuthorized + favoriteMedia(mediaId: ID!, favorite: Boolean!): Media! @isAuthorized updateUser( id: ID! username: String password: String admin: Boolean - ): User @isAdmin + ): User! @isAdmin createUser( username: String! password: String admin: Boolean! - ): User @isAdmin - deleteUser(id: ID!): User @isAdmin + ): User! @isAdmin + deleteUser(id: ID!): User! @isAdmin "Add a root path from where to look for media for the given user" userAddRootPath(id: ID!, rootPath: String!): Album @isAdmin diff --git a/ui/babel.config.js b/ui/babel.config.js index 38b42c43..8967ba44 100644 --- a/ui/babel.config.js +++ b/ui/babel.config.js @@ -9,8 +9,6 @@ module.exports = function (api) { presets.push('@babel/preset-env') plugins.push('@babel/plugin-transform-runtime') } else { - plugins.push(['styled-components', { pure: true }]) - plugins.push('graphql-tag') if (!isProduction) { plugins.push([ 'i18next-extract', @@ -20,6 +18,9 @@ module.exports = function (api) { }, ]) } + + plugins.push(['styled-components', { pure: true }]) + plugins.push('graphql-tag') } return { diff --git a/ui/build.mjs b/ui/build.mjs index c7b18a19..e8efb7bb 100644 --- a/ui/build.mjs +++ b/ui/build.mjs @@ -69,9 +69,7 @@ if (watchMode) { bs.watch('src/**/*.@(js|tsx|ts)').on('change', async args => { console.log('reloading', args) builderPromise = (await builderPromise).rebuild() - setTimeout(() => { - bs.reload(args) - }, 1000) + // bs.reload(args) }) } else { const build = async () => { diff --git a/ui/extractedTranslations/da/translation.json b/ui/extractedTranslations/da/translation.json index a385d603..5c106251 100644 --- a/ui/extractedTranslations/da/translation.json +++ b/ui/extractedTranslations/da/translation.json @@ -72,6 +72,9 @@ }, "recognize_unlabeled_faces_button": "Genkend ikke navngivede ansigter" }, + "photos_page": { + "title": "Billeder" + }, "routes": { "page_not_found": "Side ikke fundet" }, @@ -102,6 +105,10 @@ "title": "Scanner" }, "user_preferences": { + "change_language": { + "description": "Set sidens sprog specifikt for denne bruger", + "label": "Sprog" + }, "language_selector": { "placeholder": "Vælg sprog" }, @@ -211,7 +218,7 @@ "exposure": "Lukketid", "exposure_program": "Lukketid program", "flash": "Blitz", - "focal_length": "Fokallængde", + "focal_length": "Brændvidde", "iso": "ISO", "lens": "Lense", "maker": "Mærke" diff --git a/ui/extractedTranslations/en/translation.json b/ui/extractedTranslations/en/translation.json index b29d3973..09f8ced2 100644 --- a/ui/extractedTranslations/en/translation.json +++ b/ui/extractedTranslations/en/translation.json @@ -72,6 +72,9 @@ }, "recognize_unlabeled_faces_button": "Recognize unlabeled faces" }, + "photos_page": { + "title": "Photos" + }, "routes": { "page_not_found": "Page not found" }, @@ -102,6 +105,10 @@ "title": "Scanner" }, "user_preferences": { + "change_language": { + "description": "Change website language specific for this user", + "label": "Website language" + }, "language_selector": { "placeholder": "Select language" }, diff --git a/ui/src/Pages/AlbumPage/AlbumPage.js b/ui/src/Pages/AlbumPage/AlbumPage.tsx similarity index 78% rename from ui/src/Pages/AlbumPage/AlbumPage.js rename to ui/src/Pages/AlbumPage/AlbumPage.tsx index 01527b26..960111c2 100644 --- a/ui/src/Pages/AlbumPage/AlbumPage.js +++ b/ui/src/Pages/AlbumPage/AlbumPage.tsx @@ -1,16 +1,16 @@ import React, { useCallback, useEffect } from 'react' -import ReactRouterPropTypes from 'react-router-prop-types' import { useQuery, gql } from '@apollo/client' import AlbumGallery from '../../components/albumGallery/AlbumGallery' -import PropTypes from 'prop-types' import Layout from '../../Layout' -import useURLParameters from '../../hooks/useURLParameters' +import useURLParameters, { UrlKeyValuePair } from '../../hooks/useURLParameters' import useScrollPagination from '../../hooks/useScrollPagination' import PaginateLoader from '../../components/PaginateLoader' import LazyLoad from '../../helpers/LazyLoad' import { useTranslation } from 'react-i18next' +import { albumQuery, albumQueryVariables } from './__generated__/albumQuery' +import { OrderDirection } from '../../../__generated__/globalTypes' -const albumQuery = gql` +const ALBUM_QUERY = gql` query albumQuery( $id: ID! $onlyFavorites: Boolean @@ -61,7 +61,16 @@ const albumQuery = gql` let refetchNeededAll = false let refetchNeededFavorites = false -function AlbumPage({ match }) { +type AlbumPageProps = { + match: { + params: { + id: string + subPage: string + } + } +} + +function AlbumPage({ match }: AlbumPageProps) { const albumId = match.params.id const { t } = useTranslation() @@ -69,14 +78,22 @@ function AlbumPage({ match }) { const { getParam, setParam, setParams } = useURLParameters() const onlyFavorites = getParam('favorites') == '1' ? true : false - const setOnlyFavorites = favorites => setParam('favorites', favorites ? 1 : 0) + const setOnlyFavorites = (favorites: boolean) => + setParam('favorites', favorites ? '1' : '0') const orderBy = getParam('orderBy', 'date_shot') - const orderDirection = getParam('orderDirection', 'ASC') - const setOrdering = useCallback( + const orderDirStr = getParam('orderDirection', 'ASC') || 'hello' + const orderDirection = orderDirStr as OrderDirection + + type setOrderingFn = (args: { + orderBy?: string + orderDirection?: OrderDirection + }) => void + + const setOrdering: setOrderingFn = useCallback( ({ orderBy, orderDirection }) => { - let updatedParams = [] + const updatedParams: UrlKeyValuePair[] = [] if (orderBy !== undefined) { updatedParams.push({ key: 'orderBy', value: orderBy }) } @@ -89,7 +106,10 @@ function AlbumPage({ match }) { [setParams] ) - const { loading, error, data, refetch, fetchMore } = useQuery(albumQuery, { + const { loading, error, data, refetch, fetchMore } = useQuery< + albumQuery, + albumQueryVariables + >(ALBUM_QUERY, { variables: { id: albumId, onlyFavorites, @@ -100,7 +120,10 @@ function AlbumPage({ match }) { }, }) - const { containerElem, finished: finishedLoadingMore } = useScrollPagination({ + const { + containerElem, + finished: finishedLoadingMore, + } = useScrollPagination({ loading, fetchMore, data, @@ -151,7 +174,6 @@ function AlbumPage({ match }) { ref={containerElem} album={data && data.album} loading={loading} - showFavoritesToggle setOnlyFavorites={toggleFavorites} onlyFavorites={onlyFavorites} onFavorite={() => (refetchNeededAll = refetchNeededFavorites = true)} @@ -167,14 +189,4 @@ function AlbumPage({ match }) { ) } -AlbumPage.propTypes = { - ...ReactRouterPropTypes, - match: PropTypes.shape({ - params: PropTypes.shape({ - id: PropTypes.string, - subPage: PropTypes.string, - }), - }), -} - export default AlbumPage diff --git a/ui/src/Pages/AllAlbumsPage/AlbumsPage.js b/ui/src/Pages/AllAlbumsPage/AlbumsPage.tsx similarity index 100% rename from ui/src/Pages/AllAlbumsPage/AlbumsPage.js rename to ui/src/Pages/AllAlbumsPage/AlbumsPage.tsx diff --git a/ui/src/Pages/LoginPage/InitialSetupPage.js b/ui/src/Pages/LoginPage/InitialSetupPage.tsx similarity index 90% rename from ui/src/Pages/LoginPage/InitialSetupPage.js rename to ui/src/Pages/LoginPage/InitialSetupPage.tsx index 4be5b510..6f879d20 100644 --- a/ui/src/Pages/LoginPage/InitialSetupPage.js +++ b/ui/src/Pages/LoginPage/InitialSetupPage.tsx @@ -7,6 +7,7 @@ import { Container } from './loginUtilities' import { checkInitialSetupQuery, login } from './loginUtilities' import { authToken } from '../../helpers/authentication' import { useTranslation } from 'react-i18next' +import { CheckInitialSetup } from './__generated__/CheckInitialSetup' const initialSetupMutation = gql` mutation InitialSetup( @@ -35,31 +36,13 @@ const InitialSetupPage = () => { rootPath: '', }) - const handleChange = (event, key) => { - const value = event.target.value - setState(prevState => ({ - ...prevState, - [key]: value, - })) - } - - const signIn = (event, authorize) => { - event.preventDefault() - - authorize({ - variables: { - username: state.username, - password: state.password, - rootPath: state.rootPath, - }, - }) - } - if (authToken()) { return } - const { data: initialSetupData } = useQuery(checkInitialSetupQuery) + const { data: initialSetupData } = useQuery( + checkInitialSetupQuery + ) const initialSetupRedirect = initialSetupData?.siteInfo ?.initialSetup ? null : ( @@ -78,6 +61,29 @@ const InitialSetupPage = () => { }, }) + const handleChange = ( + event: React.ChangeEvent, + key: string + ) => { + const value = event.target.value + setState(prevState => ({ + ...prevState, + [key]: value, + })) + } + + const signIn = (event: React.FormEvent) => { + event.preventDefault() + + authorize({ + variables: { + username: state.username, + password: state.password, + rootPath: state.rootPath, + }, + }) + } + let errorMessage = null if (authorizationData && !authorizationData.initialSetupWizard.success) { errorMessage = authorizationData.initialSetupWizard.status @@ -93,7 +99,7 @@ const InitialSetupPage = () => {
signIn(e, authorize)} + onSubmit={signIn} loading={ authorizeLoading || authorizationData?.initialSetupWizard?.success } diff --git a/ui/src/Pages/LoginPage/LoginPage.js b/ui/src/Pages/LoginPage/LoginPage.tsx similarity index 96% rename from ui/src/Pages/LoginPage/LoginPage.js rename to ui/src/Pages/LoginPage/LoginPage.tsx index 753fd74f..389f1826 100644 --- a/ui/src/Pages/LoginPage/LoginPage.js +++ b/ui/src/Pages/LoginPage/LoginPage.tsx @@ -2,7 +2,7 @@ import React, { useState, useCallback } from 'react' import { useQuery, gql, useMutation } from '@apollo/client' import { Redirect } from 'react-router-dom' import styled from 'styled-components' -import { Button, Form, Message, Header } from 'semantic-ui-react' +import { Button, Form, Message, Header, HeaderProps } from 'semantic-ui-react' import { checkInitialSetupQuery, login, Container } from './loginUtilities' import { authToken } from '../../helpers/authentication' @@ -23,7 +23,7 @@ const StyledLogo = styled.img` max-height: 128px; ` -const LogoHeader = props => { +const LogoHeader = (props: HeaderProps) => { const { t } = useTranslation() return ( diff --git a/ui/src/Pages/LoginPage/loginUtilities.js b/ui/src/Pages/LoginPage/loginUtilities.tsx similarity index 86% rename from ui/src/Pages/LoginPage/loginUtilities.js rename to ui/src/Pages/LoginPage/loginUtilities.tsx index 32f713cb..06eb8ab5 100644 --- a/ui/src/Pages/LoginPage/loginUtilities.js +++ b/ui/src/Pages/LoginPage/loginUtilities.tsx @@ -11,9 +11,9 @@ export const checkInitialSetupQuery = gql` } ` -export function login(token) { +export function login(token: string) { saveTokenCookie(token) - window.location = '/' + window.location.href = '/' } export const Container = styled(SemanticContainer)` diff --git a/ui/src/Pages/PhotosPage/PhotosPage.js b/ui/src/Pages/PhotosPage/PhotosPage.tsx similarity index 58% rename from ui/src/Pages/PhotosPage/PhotosPage.js rename to ui/src/Pages/PhotosPage/PhotosPage.tsx index 4f0b0faa..6da5e955 100644 --- a/ui/src/Pages/PhotosPage/PhotosPage.js +++ b/ui/src/Pages/PhotosPage/PhotosPage.tsx @@ -1,24 +1,18 @@ import React from 'react' import Layout from '../../Layout' -import PropTypes from 'prop-types' import TimelineGallery from '../../components/timelineGallery/TimelineGallery' +import { useTranslation } from 'react-i18next' const PhotosPage = () => { + const { t } = useTranslation() + return ( <> - + ) } -PhotosPage.propTypes = { - match: PropTypes.shape({ - params: PropTypes.shape({ - subPage: PropTypes.string, - }), - }), -} - export default PhotosPage diff --git a/ui/src/Pages/SettingsPage/PeriodicScanner.js b/ui/src/Pages/SettingsPage/PeriodicScanner.tsx similarity index 68% rename from ui/src/Pages/SettingsPage/PeriodicScanner.js rename to ui/src/Pages/SettingsPage/PeriodicScanner.tsx index d6228907..7e22eae7 100644 --- a/ui/src/Pages/SettingsPage/PeriodicScanner.js +++ b/ui/src/Pages/SettingsPage/PeriodicScanner.tsx @@ -4,6 +4,11 @@ import { useMutation, useQuery } from '@apollo/client' import { Checkbox, Dropdown, Input, Loader } from 'semantic-ui-react' import { InputLabelDescription, InputLabelTitle } from './SettingsPage' import { useTranslation } from 'react-i18next' +import { scanIntervalQuery } from './__generated__/scanIntervalQuery' +import { + changeScanIntervalMutation, + changeScanIntervalMutationVariables, +} from './__generated__/changeScanIntervalMutation' const SCAN_INTERVAL_QUERY = gql` query scanIntervalQuery { @@ -19,44 +24,57 @@ const SCAN_INTERVAL_MUTATION = gql` } ` +enum TimeUnit { + Second = 'second', + Minute = 'minute', + Hour = 'hour', + Day = 'day', + Month = 'month', +} + const timeUnits = [ { - value: 'second', + value: TimeUnit.Second, multiplier: 1, }, { - value: 'minute', + value: TimeUnit.Minute, multiplier: 60, }, { - value: 'hour', + value: TimeUnit.Hour, multiplier: 60 * 60, }, { - value: 'day', + value: TimeUnit.Day, multiplier: 60 * 60 * 24, }, { - value: 'month', + value: TimeUnit.Month, multiplier: 60 * 60 * 24 * 30, }, ] -const convertToSeconds = ({ value, unit }) => { - return parseInt(value * timeUnits.find(x => x.value == unit).multiplier) +type TimeValue = { + value: number + unit: TimeUnit } -const convertToAppropriateUnit = ({ value, unit }) => { +const convertToSeconds = ({ value, unit }: TimeValue) => { + return value * (timeUnits.find(x => x.value == unit)?.multiplier as number) +} + +const convertToAppropriateUnit = ({ value, unit }: TimeValue): TimeValue => { if (value == 0) { return { - unit: 'second', + unit: TimeUnit.Second, value: 0, } } const seconds = convertToSeconds({ value, unit }) - let resultingUnit = timeUnits.first + let resultingUnit = timeUnits[0] for (const unit of timeUnits) { if ( seconds / unit.multiplier >= 1 && @@ -78,26 +96,26 @@ const PeriodicScanner = () => { const { t } = useTranslation() const [enablePeriodicScanner, setEnablePeriodicScanner] = useState(false) - const [scanInterval, setScanInterval] = useState({ + const [scanInterval, setScanInterval] = useState({ value: 4, - unit: 'minute', + unit: TimeUnit.Minute, }) - const scanIntervalServerValue = useRef(null) + const scanIntervalServerValue = useRef(null) - const scanIntervalQuery = useQuery(SCAN_INTERVAL_QUERY, { + const scanIntervalQuery = useQuery(SCAN_INTERVAL_QUERY, { onCompleted(data) { const queryScanInterval = data.siteInfo.periodicScanInterval if (queryScanInterval == 0) { setScanInterval({ - unit: 'second', - value: '', + unit: TimeUnit.Second, + value: 0, }) } else { setScanInterval( convertToAppropriateUnit({ - unit: 'second', + unit: TimeUnit.Second, value: queryScanInterval, }) ) @@ -110,15 +128,20 @@ const PeriodicScanner = () => { const [ setScanIntervalMutation, { loading: scanIntervalMutationLoading }, - ] = useMutation(SCAN_INTERVAL_MUTATION) + ] = useMutation< + changeScanIntervalMutation, + changeScanIntervalMutationVariables + >(SCAN_INTERVAL_MUTATION) - const onScanIntervalCheckboxChange = checked => { + const onScanIntervalCheckboxChange = (checked: boolean) => { setEnablePeriodicScanner(checked) - onScanIntervalUpdate(checked ? scanInterval : { value: 0, unit: 'second' }) + onScanIntervalUpdate( + checked ? scanInterval : { value: 0, unit: TimeUnit.Second } + ) } - const onScanIntervalUpdate = scanInterval => { + const onScanIntervalUpdate = (scanInterval: TimeValue) => { const seconds = convertToSeconds(scanInterval) if (scanIntervalServerValue.current != seconds) { @@ -131,31 +154,37 @@ const PeriodicScanner = () => { } } - const scanIntervalUnits = [ + type scanIntervalUnitType = { + key: TimeUnit + text: string + value: TimeUnit + } + + const scanIntervalUnits: scanIntervalUnitType[] = [ { - key: 'second', + key: TimeUnit.Second, text: t('settings.periodic_scanner.interval_unit.seconds', 'Seconds'), - value: 'second', + value: TimeUnit.Second, }, { - key: 'minute', + key: TimeUnit.Minute, text: t('settings.periodic_scanner.interval_unit.minutes', 'Minutes'), - value: 'minute', + value: TimeUnit.Minute, }, { - key: 'hour', + key: TimeUnit.Hour, text: t('settings.periodic_scanner.interval_unit.hour', 'Hour'), - value: 'hour', + value: TimeUnit.Hour, }, { - key: 'day', + key: TimeUnit.Day, text: t('settings.periodic_scanner.interval_unit.days', 'Days'), - value: 'day', + value: TimeUnit.Day, }, { - key: 'month', + key: TimeUnit.Month, text: t('settings.periodic_scanner.interval_unit.months', 'Months'), - value: 'month', + value: TimeUnit.Month, }, ] @@ -171,7 +200,9 @@ const PeriodicScanner = () => { )} disabled={scanIntervalQuery.loading} checked={enablePeriodicScanner} - onChange={(_, { checked }) => onScanIntervalCheckboxChange(checked)} + onChange={(_, { checked }) => + onScanIntervalCheckboxChange(checked || false) + } /> @@ -195,9 +226,9 @@ const PeriodicScanner = () => { label={ { - const newScanInterval = { + const newScanInterval: TimeValue = { ...scanInterval, - unit: value, + unit: value as TimeUnit, } setScanInterval(newScanInterval) @@ -208,7 +239,7 @@ const PeriodicScanner = () => { /> } onBlur={() => onScanIntervalUpdate(scanInterval)} - onKeyDown={({ key }) => + onKeyDown={({ key }: KeyboardEvent) => key == 'Enter' && onScanIntervalUpdate(scanInterval) } loading={scanIntervalQuery.loading} @@ -219,7 +250,7 @@ const PeriodicScanner = () => { onChange={(_, { value }) => { setScanInterval(x => ({ ...x, - value, + value: parseInt(value), })) }} /> diff --git a/ui/src/Pages/SettingsPage/ScannerConcurrentWorkers.js b/ui/src/Pages/SettingsPage/ScannerConcurrentWorkers.tsx similarity index 68% rename from ui/src/Pages/SettingsPage/ScannerConcurrentWorkers.js rename to ui/src/Pages/SettingsPage/ScannerConcurrentWorkers.tsx index 9d736d76..3d6fc9f3 100644 --- a/ui/src/Pages/SettingsPage/ScannerConcurrentWorkers.js +++ b/ui/src/Pages/SettingsPage/ScannerConcurrentWorkers.tsx @@ -3,6 +3,11 @@ import { useQuery, useMutation, gql } from '@apollo/client' import { Input, Loader } from 'semantic-ui-react' import { InputLabelTitle, InputLabelDescription } from './SettingsPage' import { useTranslation } from 'react-i18next' +import { concurrentWorkersQuery } from './__generated__/concurrentWorkersQuery' +import { + setConcurrentWorkers, + setConcurrentWorkersVariables, +} from './__generated__/setConcurrentWorkers' const CONCURRENT_WORKERS_QUERY = gql` query concurrentWorkersQuery { @@ -21,21 +26,25 @@ const SET_CONCURRENT_WORKERS_MUTATION = gql` const ScannerConcurrentWorkers = () => { const { t } = useTranslation() - const workerAmountQuery = useQuery(CONCURRENT_WORKERS_QUERY, { - onCompleted(data) { - setWorkerAmount(data.siteInfo.concurrentWorkers) - workerAmountServerValue.current = data.siteInfo.concurrentWorkers - }, - }) - - const [setWorkersMutation, workersMutationData] = useMutation( - SET_CONCURRENT_WORKERS_MUTATION + const workerAmountQuery = useQuery( + CONCURRENT_WORKERS_QUERY, + { + onCompleted(data) { + setWorkerAmount(data.siteInfo.concurrentWorkers) + workerAmountServerValue.current = data.siteInfo.concurrentWorkers + }, + } ) - const workerAmountServerValue = useRef(null) - const [workerAmount, setWorkerAmount] = useState('') + const [setWorkersMutation, workersMutationData] = useMutation< + setConcurrentWorkers, + setConcurrentWorkersVariables + >(SET_CONCURRENT_WORKERS_MUTATION) - const updateWorkerAmount = workerAmount => { + const workerAmountServerValue = useRef(null) + const [workerAmount, setWorkerAmount] = useState(0) + + const updateWorkerAmount = (workerAmount: number) => { if (workerAmountServerValue.current != workerAmount) { workerAmountServerValue.current = workerAmount setWorkersMutation({ @@ -67,10 +76,10 @@ const ScannerConcurrentWorkers = () => { id="scanner_concurrent_workers_field" value={workerAmount} onChange={(_, { value }) => { - setWorkerAmount(value) + setWorkerAmount(parseInt(value)) }} onBlur={() => updateWorkerAmount(workerAmount)} - onKeyDown={({ key }) => + onKeyDown={({ key }: KeyboardEvent) => key == 'Enter' && updateWorkerAmount(workerAmount) } /> diff --git a/ui/src/Pages/SettingsPage/ScannerSection.js b/ui/src/Pages/SettingsPage/ScannerSection.tsx similarity index 89% rename from ui/src/Pages/SettingsPage/ScannerSection.js rename to ui/src/Pages/SettingsPage/ScannerSection.tsx index ce14420d..77070c76 100644 --- a/ui/src/Pages/SettingsPage/ScannerSection.js +++ b/ui/src/Pages/SettingsPage/ScannerSection.tsx @@ -5,6 +5,7 @@ import PeriodicScanner from './PeriodicScanner' import ScannerConcurrentWorkers from './ScannerConcurrentWorkers' import { SectionTitle, InputLabelDescription } from './SettingsPage' import { useTranslation } from 'react-i18next' +import { scanAllMutation } from './__generated__/scanAllMutation' const SCAN_MUTATION = gql` mutation scanAllMutation { @@ -17,7 +18,7 @@ const SCAN_MUTATION = gql` const ScannerSection = () => { const { t } = useTranslation() - const [startScanner, { called }] = useMutation(SCAN_MUTATION) + const [startScanner, { called }] = useMutation(SCAN_MUTATION) return (
diff --git a/ui/src/Pages/SettingsPage/SettingsPage.tsx b/ui/src/Pages/SettingsPage/SettingsPage.tsx index b9d02691..c8b1dbf6 100644 --- a/ui/src/Pages/SettingsPage/SettingsPage.tsx +++ b/ui/src/Pages/SettingsPage/SettingsPage.tsx @@ -10,7 +10,7 @@ import ScannerSection from './ScannerSection' import UserPreferences from './UserPreferences' import UsersTable from './Users/UsersTable' -export const SectionTitle = styled.h2<{ nospace: boolean }>` +export const SectionTitle = styled.h2<{ nospace?: boolean }>` margin-top: ${({ nospace }) => (nospace ? '0' : '1.4em')} !important; padding-bottom: 0.3em; border-bottom: 1px solid #ddd; diff --git a/ui/src/Pages/SettingsPage/UserPreferences.tsx b/ui/src/Pages/SettingsPage/UserPreferences.tsx index 3149286c..c5bf833f 100644 --- a/ui/src/Pages/SettingsPage/UserPreferences.tsx +++ b/ui/src/Pages/SettingsPage/UserPreferences.tsx @@ -5,7 +5,11 @@ import { useTranslation } from 'react-i18next' import { Dropdown } from 'semantic-ui-react' import styled from 'styled-components' import { LanguageTranslation } from '../../../__generated__/globalTypes' -import { SectionTitle } from './SettingsPage' +import { + InputLabelDescription, + InputLabelTitle, + SectionTitle, +} from './SettingsPage' import { changeUserPreferences, changeUserPreferencesVariables, @@ -13,8 +17,8 @@ import { import { myUserPreferences } from './__generated__/myUserPreferences' const languagePreferences = [ - { key: 1, text: 'English', value: LanguageTranslation.English }, - { key: 2, text: 'Dansk', value: LanguageTranslation.Danish }, + { key: 1, text: 'English', flag: 'uk', value: LanguageTranslation.English }, + { key: 2, text: 'Dansk', flag: 'dk', value: LanguageTranslation.Danish }, ] const CHANGE_USER_PREFERENCES = gql` @@ -58,7 +62,22 @@ const UserPreferences = () => { {t('settings.user_preferences.title', 'User preferences')} + { }) }} selection + search value={data?.myUserPreferences.language || undefined} loading={loadingPrefs} disabled={loadingPrefs} diff --git a/ui/src/Pages/SettingsPage/Users/AddUserRow.js b/ui/src/Pages/SettingsPage/Users/AddUserRow.tsx similarity index 86% rename from ui/src/Pages/SettingsPage/Users/AddUserRow.js rename to ui/src/Pages/SettingsPage/Users/AddUserRow.tsx index 831239ff..efb76387 100644 --- a/ui/src/Pages/SettingsPage/Users/AddUserRow.js +++ b/ui/src/Pages/SettingsPage/Users/AddUserRow.tsx @@ -1,10 +1,9 @@ import { gql, useMutation } from '@apollo/client' -import PropTypes from 'prop-types' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { Button, Checkbox, Input, Table } from 'semantic-ui-react' -const createUserMutation = gql` +const CREATE_USER_MUTATION = gql` mutation createUser($username: String!, $admin: Boolean!) { createUser(username: $username, admin: $admin) { id @@ -15,7 +14,7 @@ const createUserMutation = gql` } ` -export const userAddRootPathMutation = gql` +export const USER_ADD_ROOT_PATH_MUTATION = gql` mutation userAddRootPath($id: ID!, $rootPath: String!) { userAddRootPath(id: $id, rootPath: $rootPath) { id @@ -30,12 +29,18 @@ const initialState = { userAdded: false, } -const AddUserRow = ({ setShow, show, onUserAdded }) => { +type AddUserRowProps = { + setShow: React.Dispatch> + show: boolean + onUserAdded(): void +} + +const AddUserRow = ({ setShow, show, onUserAdded }: AddUserRowProps) => { const { t } = useTranslation() const [state, setState] = useState(initialState) const [addRootPath, { loading: addRootPathLoading }] = useMutation( - userAddRootPathMutation, + USER_ADD_ROOT_PATH_MUTATION, { onCompleted: () => { setState(initialState) @@ -49,7 +54,7 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => { ) const [createUser, { loading: createUserLoading }] = useMutation( - createUserMutation, + CREATE_USER_MUTATION, { onCompleted: ({ createUser: { id } }) => { if (state.rootPath) { @@ -68,7 +73,10 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => { const loading = addRootPathLoading || createUserLoading - function updateInput(event, key) { + function updateInput( + event: React.ChangeEvent, + key: string + ) { setState({ ...state, [key]: event.target.value, @@ -105,7 +113,7 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => { onChange={(e, data) => { setState({ ...state, - admin: data.checked, + admin: data.checked || false, }) }} /> @@ -137,10 +145,4 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => { ) } -AddUserRow.propTypes = { - setShow: PropTypes.func.isRequired, - show: PropTypes.bool.isRequired, - onUserAdded: PropTypes.func.isRequired, -} - export default AddUserRow diff --git a/ui/src/Pages/SettingsPage/Users/EditUserRow.js b/ui/src/Pages/SettingsPage/Users/EditUserRow.tsx similarity index 87% rename from ui/src/Pages/SettingsPage/Users/EditUserRow.js rename to ui/src/Pages/SettingsPage/Users/EditUserRow.tsx index 3c5d9f2c..9f030502 100644 --- a/ui/src/Pages/SettingsPage/Users/EditUserRow.js +++ b/ui/src/Pages/SettingsPage/Users/EditUserRow.tsx @@ -2,7 +2,7 @@ import React from 'react' import { useTranslation } from 'react-i18next' import { Button, Checkbox, Input, Table } from 'semantic-ui-react' import { EditRootPaths } from './EditUserRowRootPaths' -import { UserRowProps } from './UserRow' +import { UserRowProps, UserRowChildProps } from './UserRow' const EditUserRow = ({ user, @@ -10,9 +10,13 @@ const EditUserRow = ({ setState, updateUser, updateUserLoading, -}) => { +}: UserRowChildProps) => { const { t } = useTranslation() - function updateInput(event, key) { + + function updateInput( + event: React.ChangeEvent, + key: string + ) { setState(state => ({ ...state, [key]: event.target.value, @@ -39,7 +43,7 @@ const EditUserRow = ({ onChange={(_, data) => { setState(state => ({ ...state, - admin: data.checked, + admin: data.checked || false, })) }} /> @@ -50,6 +54,7 @@ const EditUserRow = ({ negative onClick={() => setState(state => ({ + ...state, ...state.oldState, })) } diff --git a/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.js b/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.tsx similarity index 65% rename from ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.js rename to ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.tsx index ddc5c00e..1dacddb0 100644 --- a/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.js +++ b/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.tsx @@ -1,13 +1,21 @@ -import PropTypes from 'prop-types' import React, { useState } from 'react' import { gql, useMutation } from '@apollo/client' import { Button, Icon, Input } from 'semantic-ui-react' import styled from 'styled-components' import { USERS_QUERY } from './UsersTable' import { useTranslation } from 'react-i18next' -import { userAddRootPathMutation } from './AddUserRow' +import { USER_ADD_ROOT_PATH_MUTATION } from './AddUserRow' +import { + userRemoveAlbumPathMutation, + userRemoveAlbumPathMutationVariables, +} from './__generated__/userRemoveAlbumPathMutation' +import { + settingsUsersQuery_user, + settingsUsersQuery_user_rootAlbums, +} from './__generated__/settingsUsersQuery' +import { userAddRootPath } from './__generated__/userAddRootPath' -const userRemoveAlbumPathMutation = gql` +const USER_REMOVE_ALBUM_PATH_MUTATION = gql` mutation userRemoveAlbumPathMutation($userId: ID!, $albumId: ID!) { userRemoveRootAlbum(userId: $userId, albumId: $albumId) { id @@ -21,18 +29,23 @@ const RootPathListItem = styled.li` align-items: center; ` -const EditRootPath = ({ album, user }) => { +type EditRootPathProps = { + album: settingsUsersQuery_user_rootAlbums + user: settingsUsersQuery_user +} + +const EditRootPath = ({ album, user }: EditRootPathProps) => { const { t } = useTranslation() - const [removeAlbumPath, { loading }] = useMutation( + const [removeAlbumPath, { loading }] = useMutation< userRemoveAlbumPathMutation, - { - refetchQueries: [ - { - query: USERS_QUERY, - }, - ], - } - ) + userRemoveAlbumPathMutationVariables + >(USER_REMOVE_ALBUM_PATH_MUTATION, { + refetchQueries: [ + { + query: USERS_QUERY, + }, + ], + }) return ( @@ -56,33 +69,37 @@ const EditRootPath = ({ album, user }) => { ) } -EditRootPath.propTypes = { - album: PropTypes.object.isRequired, - user: PropTypes.object.isRequired, -} - const NewRootPathInput = styled(Input)` width: 100%; margin-top: 24px; ` -const EditNewRootPath = ({ userID }) => { +type EditNewRootPathProps = { + userID: string +} + +const EditNewRootPath = ({ userID }: EditNewRootPathProps) => { const { t } = useTranslation() const [value, setValue] = useState('') - const [addRootPath, { loading }] = useMutation(userAddRootPathMutation, { - refetchQueries: [ - { - query: USERS_QUERY, - }, - ], - }) + const [addRootPath, { loading }] = useMutation( + USER_ADD_ROOT_PATH_MUTATION, + { + refetchQueries: [ + { + query: USERS_QUERY, + }, + ], + } + ) return (
  • setValue(e.target.value)} + onChange={(e: React.ChangeEvent) => + setValue(e.target.value) + } disabled={loading} action={{ positive: true, @@ -103,17 +120,17 @@ const EditNewRootPath = ({ userID }) => { ) } -EditNewRootPath.propTypes = { - userID: PropTypes.string.isRequired, -} - const RootPathList = styled.ul` margin: 0; padding: 0; list-style: none; ` -export const EditRootPaths = ({ user }) => { +type EditRootPathsProps = { + user: settingsUsersQuery_user +} + +export const EditRootPaths = ({ user }: EditRootPathsProps) => { const editRows = user.rootAlbums.map(album => ( )) @@ -125,7 +142,3 @@ export const EditRootPaths = ({ user }) => { ) } - -EditRootPaths.propTypes = { - user: PropTypes.object.isRequired, -} diff --git a/ui/src/Pages/SettingsPage/Users/UserChangePassword.js b/ui/src/Pages/SettingsPage/Users/UserChangePassword.tsx similarity index 82% rename from ui/src/Pages/SettingsPage/Users/UserChangePassword.js rename to ui/src/Pages/SettingsPage/Users/UserChangePassword.tsx index d7a537e2..d807bd7b 100644 --- a/ui/src/Pages/SettingsPage/Users/UserChangePassword.js +++ b/ui/src/Pages/SettingsPage/Users/UserChangePassword.tsx @@ -1,8 +1,8 @@ import React, { useState } from 'react' -import PropTypes from 'prop-types' import { gql, useMutation } from '@apollo/client' -import { Button, Form, Input, Modal } from 'semantic-ui-react' +import { Button, Form, Input, Modal, ModalProps } from 'semantic-ui-react' import { Trans, useTranslation } from 'react-i18next' +import { settingsUsersQuery_user } from './__generated__/settingsUsersQuery' const changeUserPasswordMutation = gql` mutation changeUserPassword($userId: ID!, $password: String!) { @@ -12,7 +12,18 @@ const changeUserPasswordMutation = gql` } ` -const ChangePasswordModal = ({ onClose, user, ...props }) => { +interface ChangePasswordModalProps extends ModalProps { + onClose(): void + open: boolean + user: settingsUsersQuery_user +} + +const ChangePasswordModal = ({ + onClose, + user, + open, + ...props +}: ChangePasswordModalProps) => { const { t } = useTranslation() const [passwordInput, setPasswordInput] = useState('') @@ -23,7 +34,7 @@ const ChangePasswordModal = ({ onClose, user, ...props }) => { }) return ( - + {t('settings.users.password_reset.title', 'Change password')} @@ -71,9 +82,4 @@ const ChangePasswordModal = ({ onClose, user, ...props }) => { ) } -ChangePasswordModal.propTypes = { - onClose: PropTypes.func, - user: PropTypes.object.isRequired, -} - export default ChangePasswordModal diff --git a/ui/src/Pages/SettingsPage/Users/UserRow.js b/ui/src/Pages/SettingsPage/Users/UserRow.js deleted file mode 100644 index 05728b9f..00000000 --- a/ui/src/Pages/SettingsPage/Users/UserRow.js +++ /dev/null @@ -1,111 +0,0 @@ -import PropTypes from 'prop-types' -import React, { useState } from 'react' -import { gql, useMutation } from '@apollo/client' -import EditUserRow from './EditUserRow' -import ViewUserRow from './ViewUserRow' - -const updateUserMutation = gql` - mutation updateUser($id: ID!, $username: String, $admin: Boolean) { - updateUser(id: $id, username: $username, admin: $admin) { - id - username - admin - } - } -` - -const deleteUserMutation = gql` - mutation deleteUser($id: ID!) { - deleteUser(id: $id) { - id - username - } - } -` - -const scanUserMutation = gql` - mutation scanUser($userId: ID!) { - scanUser(userId: $userId) { - success - } - } -` - -const UserRow = ({ user, refetchUsers }) => { - const [state, setState] = useState({ - ...user, - editing: false, - newRootPath: '', - }) - - const [showConfirmDelete, setConfirmDelete] = useState(false) - const [showChangePassword, setChangePassword] = useState(false) - - const [updateUser, { loading: updateUserLoading }] = useMutation( - updateUserMutation, - { - onCompleted: data => { - setState({ - ...data.updateUser, - editing: false, - }) - refetchUsers() - }, - } - ) - - const [deleteUser] = useMutation(deleteUserMutation, { - onCompleted: () => { - refetchUsers() - }, - }) - - const [scanUser, { called: scanUserCalled }] = useMutation(scanUserMutation, { - onCompleted: () => { - refetchUsers() - }, - }) - - const props = { - user, - state, - setState, - scanUser, - updateUser, - updateUserLoading, - deleteUser, - setChangePassword, - setConfirmDelete, - scanUserCalled, - showChangePassword, - showConfirmDelete, - } - - if (state.editing) { - return - } - - return -} - -UserRow.propTypes = { - user: PropTypes.object.isRequired, - refetchUsers: PropTypes.func.isRequired, -} - -export const UserRowProps = { - user: PropTypes.object.isRequired, - state: PropTypes.object.isRequired, - setState: PropTypes.func.isRequired, - scanUser: PropTypes.func.isRequired, - updateUser: PropTypes.func.isRequired, - updateUserLoading: PropTypes.bool.isRequired, - deleteUser: PropTypes.func.isRequired, - setChangePassword: PropTypes.func.isRequired, - setConfirmDelete: PropTypes.func.isRequired, - scanUserCalled: PropTypes.func.isRequired, - showChangePassword: PropTypes.func.isRequired, - showConfirmDelete: PropTypes.func.isRequired, -} - -export default UserRow diff --git a/ui/src/Pages/SettingsPage/Users/UserRow.tsx b/ui/src/Pages/SettingsPage/Users/UserRow.tsx new file mode 100644 index 00000000..23243c38 --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/UserRow.tsx @@ -0,0 +1,153 @@ +import PropTypes from 'prop-types' +import React, { useState } from 'react' +import { + FetchResult, + gql, + MutationFunctionOptions, + useMutation, +} from '@apollo/client' +import EditUserRow from './EditUserRow' +import ViewUserRow from './ViewUserRow' +import { settingsUsersQuery_user } from './__generated__/settingsUsersQuery' +import { scanUser, scanUserVariables } from './__generated__/scanUser' +import { updateUser, updateUserVariables } from './__generated__/updateUser' +import { deleteUser, deleteUserVariables } from './__generated__/deleteUser' + +const updateUserMutation = gql` + mutation updateUser($id: ID!, $username: String, $admin: Boolean) { + updateUser(id: $id, username: $username, admin: $admin) { + id + username + admin + } + } +` + +const deleteUserMutation = gql` + mutation deleteUser($id: ID!) { + deleteUser(id: $id) { + id + username + } + } +` + +const scanUserMutation = gql` + mutation scanUser($userId: ID!) { + scanUser(userId: $userId) { + success + } + } +` + +export const UserRowProps = { + user: PropTypes.object.isRequired, + state: PropTypes.object.isRequired, + setState: PropTypes.func.isRequired, + scanUser: PropTypes.func.isRequired, + updateUser: PropTypes.func.isRequired, + updateUserLoading: PropTypes.bool.isRequired, + deleteUser: PropTypes.func.isRequired, + setChangePassword: PropTypes.func.isRequired, + setConfirmDelete: PropTypes.func.isRequired, + scanUserCalled: PropTypes.func.isRequired, + showChangePassword: PropTypes.func.isRequired, + showConfirmDelete: PropTypes.func.isRequired, +} + +interface UserRowState extends settingsUsersQuery_user { + editing: boolean + newRootPath: string + oldState?: Omit +} + +type ApolloMutationFn = ( + options?: MutationFunctionOptions + // eslint-disable-next-line @typescript-eslint/no-explicit-any +) => Promise> + +export type UserRowChildProps = { + user: settingsUsersQuery_user + state: UserRowState + setState: React.Dispatch> + scanUser: ApolloMutationFn + updateUser: ApolloMutationFn + updateUserLoading: boolean + deleteUser: ApolloMutationFn + setChangePassword: React.Dispatch> + setConfirmDelete: React.Dispatch> + scanUserCalled: boolean + showChangePassword: boolean + showConfirmDelete: boolean +} + +type UserRowProps = { + user: settingsUsersQuery_user + refetchUsers(): void +} + +const UserRow = ({ user, refetchUsers }: UserRowProps) => { + const [state, setState] = useState({ + ...user, + editing: false, + newRootPath: '', + }) + + const [showConfirmDelete, setConfirmDelete] = useState(false) + const [showChangePassword, setChangePassword] = useState(false) + + const [updateUser, { loading: updateUserLoading }] = useMutation< + updateUser, + updateUserVariables + >(updateUserMutation, { + onCompleted: data => { + setState(state => ({ + ...state, + ...data.updateUser, + editing: false, + })) + refetchUsers() + }, + }) + + const [deleteUser] = useMutation( + deleteUserMutation, + { + onCompleted: () => { + refetchUsers() + }, + } + ) + + const [scanUser, { called: scanUserCalled }] = useMutation< + scanUser, + scanUserVariables + >(scanUserMutation, { + onCompleted: () => { + refetchUsers() + }, + }) + + const props: UserRowChildProps = { + user, + state, + setState, + scanUser, + updateUser, + updateUserLoading, + deleteUser, + setChangePassword, + setConfirmDelete, + scanUserCalled, + showChangePassword, + showConfirmDelete, + } + + if (state.editing) { + return + } + + return +} + +export default UserRow diff --git a/ui/src/Pages/SettingsPage/Users/UsersTable.js b/ui/src/Pages/SettingsPage/Users/UsersTable.tsx similarity index 91% rename from ui/src/Pages/SettingsPage/Users/UsersTable.js rename to ui/src/Pages/SettingsPage/Users/UsersTable.tsx index 4d7f21f5..b905ecdb 100644 --- a/ui/src/Pages/SettingsPage/Users/UsersTable.js +++ b/ui/src/Pages/SettingsPage/Users/UsersTable.tsx @@ -6,13 +6,13 @@ import UserRow from './UserRow' import AddUserRow from './AddUserRow' import { SectionTitle } from '../SettingsPage' import { useTranslation } from 'react-i18next' +import { settingsUsersQuery } from './__generated__/settingsUsersQuery' export const USERS_QUERY = gql` query settingsUsersQuery { user { id username - # rootPath admin rootAlbums { id @@ -26,14 +26,16 @@ const UsersTable = () => { const { t } = useTranslation() const [showAddUser, setShowAddUser] = useState(false) - const { loading, error, data, refetch } = useQuery(USERS_QUERY) + const { loading, error, data, refetch } = useQuery( + USERS_QUERY + ) if (error) { return
    {`Users table error: ${error.message}`}
    } - let userRows = [] - if (data && data.user) { + let userRows: JSX.Element[] = [] + if (data?.user) { userRows = data.user.map(user => ( )) diff --git a/ui/src/Pages/SettingsPage/Users/ViewUserRow.js b/ui/src/Pages/SettingsPage/Users/ViewUserRow.tsx similarity index 92% rename from ui/src/Pages/SettingsPage/Users/ViewUserRow.js rename to ui/src/Pages/SettingsPage/Users/ViewUserRow.tsx index b2d6ef3a..f4b5b7ec 100644 --- a/ui/src/Pages/SettingsPage/Users/ViewUserRow.js +++ b/ui/src/Pages/SettingsPage/Users/ViewUserRow.tsx @@ -3,7 +3,7 @@ import { Trans, useTranslation } from 'react-i18next' import { Button, Icon, Table, Modal } from 'semantic-ui-react' import styled from 'styled-components' import ChangePasswordModal from './UserChangePassword' -import { UserRowProps } from './UserRow' +import { UserRowChildProps, UserRowProps } from './UserRow' const PathList = styled.ul` margin: 0; @@ -22,7 +22,7 @@ const ViewUserRow = ({ scanUserCalled, showChangePassword, showConfirmDelete, -}) => { +}: UserRowChildProps) => { const { t } = useTranslation() const paths = ( @@ -43,7 +43,11 @@ const ViewUserRow = ({