🚸 (account) Improve account form and fix cyclic dependencies

This commit is contained in:
Baptiste Arnaud
2023-01-18 11:40:38 +01:00
parent c711f3660f
commit 49058da206
12 changed files with 112 additions and 154 deletions

View File

@@ -5,27 +5,28 @@ import {
ReactNode,
useContext,
useEffect,
useMemo,
useState,
} from 'react'
import { isDefined, isNotDefined } from 'utils'
import { dequal } from 'dequal'
import { env, isDefined, isNotDefined } from 'utils'
import { User } from 'db'
import { setUser as setSentryUser } from '@sentry/nextjs'
import { useToast } from '@/hooks/useToast'
import { updateUserQuery } from './queries/updateUserQuery'
import { useDebouncedCallback } from 'use-debounce'
const userContext = createContext<{
user?: User
isLoading: boolean
isSaving: boolean
hasUnsavedChanges: boolean
currentWorkspaceId?: string
updateUser: (newUser: Partial<User>) => void
saveUser: (newUser?: Partial<User>) => Promise<void>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})
}>({
isLoading: false,
updateUser: () => {
console.log('updateUser not implemented')
},
})
const debounceTimeout = 1000
export const UserProvider = ({ children }: { children: ReactNode }) => {
const router = useRouter()
@@ -34,13 +35,6 @@ export const UserProvider = ({ children }: { children: ReactNode }) => {
const { showToast } = useToast()
const [currentWorkspaceId, setCurrentWorkspaceId] = useState<string>()
const [isSaving, setIsSaving] = useState(false)
const hasUnsavedChanges = useMemo(
() => !dequal(session?.user, user),
[session?.user, user]
)
useEffect(() => {
if (isDefined(user) || isNotDefined(session)) return
setCurrentWorkspaceId(
@@ -70,31 +64,36 @@ export const UserProvider = ({ children }: { children: ReactNode }) => {
const isSigningIn = () => ['/signin', '/register'].includes(router.pathname)
const updateUser = (newUser: Partial<User>) => {
const updateUser = (updates: Partial<User>) => {
if (isNotDefined(user)) return
setUser({ ...user, ...newUser })
const newUser = { ...user, ...updates }
setUser(newUser)
saveUser(newUser)
}
const saveUser = async (newUser?: Partial<User>) => {
if (isNotDefined(user)) return
setIsSaving(true)
if (newUser) updateUser(newUser)
const { error } = await updateUserQuery(user.id, { ...user, ...newUser })
if (error) showToast({ title: error.name, description: error.message })
await refreshUser()
setIsSaving(false)
}
const saveUser = useDebouncedCallback(
async (newUser?: Partial<User>) => {
if (isNotDefined(user)) return
const { error } = await updateUserQuery(user.id, { ...user, ...newUser })
if (error) showToast({ title: error.name, description: error.message })
await refreshUser()
},
env('E2E_TEST') === 'true' ? 0 : debounceTimeout
)
useEffect(() => {
return () => {
saveUser.flush()
}
}, [saveUser])
return (
<userContext.Provider
value={{
user,
isSaving,
isLoading: status === 'loading',
hasUnsavedChanges,
currentWorkspaceId,
updateUser,
saveUser,
}}
>
{children}

View File

@@ -7,13 +7,10 @@ test.describe.configure({ mode: 'parallel' })
test('should display user info properly', async ({ page }) => {
await page.goto('/typebots')
await page.click('text=Settings & Members')
const saveButton = page.locator('button:has-text("Save")')
await expect(saveButton).toBeHidden()
expect(
page.locator('input[type="email"]').getAttribute('disabled')
).toBeDefined()
await page.fill('#name', 'John Doe')
expect(saveButton).toBeVisible()
await page.getByRole('textbox', { name: 'Name:' }).fill('John Doe')
await page.setInputFiles('input[type="file"]', getTestAsset('avatar.jpg'))
await expect(page.locator('img >> nth=1')).toHaveAttribute(
'src',

View File

@@ -1,35 +1,28 @@
import {
Stack,
HStack,
Avatar,
Button,
FormControl,
FormLabel,
Input,
Tooltip,
Flex,
Text,
} from '@chakra-ui/react'
import { Stack, HStack, Avatar, Text, Tooltip } from '@chakra-ui/react'
import { UploadIcon } from '@/components/icons'
import React, { ChangeEvent } from 'react'
import { isDefined } from 'utils'
import React, { useState } from 'react'
import { ApiTokensList } from './ApiTokensList'
import { UploadButton } from '@/components/ImageUploadContent/UploadButton'
import { useUser } from '@/features/account'
import { Input } from '@/components/inputs/Input'
export const MyAccountForm = () => {
const { user, updateUser, saveUser, hasUnsavedChanges, isSaving } = useUser()
const { user, updateUser } = useUser()
const [name, setName] = useState(user?.name ?? '')
const [email, setEmail] = useState(user?.email ?? '')
const handleFileUploaded = async (url: string) => {
updateUser({ image: url })
}
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => {
updateUser({ name: e.target.value })
const handleNameChange = (newName: string) => {
setName(newName)
updateUser({ name: newName })
}
const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) => {
updateUser({ email: e.target.value })
const handleEmailChange = (newEmail: string) => {
setEmail(newEmail)
updateUser({ email: newEmail })
}
return (
@@ -56,40 +49,26 @@ export const MyAccountForm = () => {
</Stack>
</HStack>
<FormControl>
<FormLabel htmlFor="name">Name</FormLabel>
<Input id="name" value={user?.name ?? ''} onChange={handleNameChange} />
</FormControl>
{isDefined(user?.email) && (
<Tooltip
label="Updating email is not available."
placement="left"
hasArrow
>
<FormControl>
<FormLabel htmlFor="email">Email address</FormLabel>
<Input
id="email"
type="email"
isDisabled
value={user?.email ?? ''}
onChange={handleEmailChange}
/>
</FormControl>
</Tooltip>
)}
{hasUnsavedChanges && (
<Flex justifyContent="flex-end">
<Button
colorScheme="blue"
onClick={() => saveUser()}
isLoading={isSaving}
>
Save
</Button>
</Flex>
)}
<Input
value={name}
onChange={handleNameChange}
label="Name:"
withVariableButton={false}
debounceTimeout={0}
/>
<Tooltip label="Updating email is not available. Contact the support if you want to change it.">
<span>
<Input
type="email"
value={email}
onChange={handleEmailChange}
label="Email address:"
withVariableButton={false}
debounceTimeout={0}
isDisabled
/>
</span>
</Tooltip>
{user && <ApiTokensList user={user} />}
</Stack>

View File

@@ -7,20 +7,20 @@ import { AppearanceRadioGroup } from './AppearanceRadioGroup'
export const UserPreferencesForm = () => {
const { setColorMode } = useColorMode()
const { saveUser, user } = useUser()
const { user, updateUser } = useUser()
useEffect(() => {
if (!user?.graphNavigation)
saveUser({ graphNavigation: GraphNavigation.TRACKPAD })
}, [saveUser, user?.graphNavigation])
updateUser({ graphNavigation: GraphNavigation.TRACKPAD })
}, [updateUser, user?.graphNavigation])
const changeGraphNavigation = async (value: string) => {
await saveUser({ graphNavigation: value as GraphNavigation })
updateUser({ graphNavigation: value as GraphNavigation })
}
const changeAppearance = async (value: string) => {
setColorMode(value)
await saveUser({ preferredAppAppearance: value })
updateUser({ preferredAppAppearance: value })
}
return (