🚸 (account) Improve account form and fix cyclic dependencies
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user