🛂 Improve editor authorization feedback (#856)
Closes #844, closes #839 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ### Summary by CodeRabbit - New Feature: Added a `logOut` function to the user context for improved logout handling. - Refactor: Updated the redirect path in the `SignInForm` component for better user redirection after authentication. - New Feature: Enhanced the "Add" button and "Connect new" menu item in `CredentialsDropdown` with role-based access control. - Refactor: Replaced the `signOut` function with the `logOut` function from the `useUser` hook in `DashboardHeader`. - Bug Fix: Prevented execution of certain code blocks in `TypebotProvider` when `typebotData` is read-only. - Refactor: Optimized the `handleObserver` function in `ResultsTable` with a `useCallback` hook. - Bug Fix: Improved router readiness check in `WorkspaceProvider` to prevent premature execution of certain operations. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useSession } from 'next-auth/react'
|
import { signOut, useSession } from 'next-auth/react'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { createContext, ReactNode, useEffect, useState } from 'react'
|
import { createContext, ReactNode, useEffect, useState } from 'react'
|
||||||
import { isDefined, isNotDefined } from '@typebot.io/lib'
|
import { isDefined, isNotDefined } from '@typebot.io/lib'
|
||||||
@@ -15,9 +15,13 @@ export const userContext = createContext<{
|
|||||||
user?: User
|
user?: User
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
currentWorkspaceId?: string
|
currentWorkspaceId?: string
|
||||||
|
logOut: () => void
|
||||||
updateUser: (newUser: Partial<User>) => void
|
updateUser: (newUser: Partial<User>) => void
|
||||||
}>({
|
}>({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
logOut: () => {
|
||||||
|
console.log('logOut not implemented')
|
||||||
|
},
|
||||||
updateUser: () => {
|
updateUser: () => {
|
||||||
console.log('updateUser not implemented')
|
console.log('updateUser not implemented')
|
||||||
},
|
},
|
||||||
@@ -91,6 +95,11 @@ export const UserProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout
|
env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const logOut = () => {
|
||||||
|
signOut()
|
||||||
|
setUser(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
saveUser.flush()
|
saveUser.flush()
|
||||||
@@ -103,6 +112,7 @@ export const UserProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
user,
|
user,
|
||||||
isLoading: status === 'loading',
|
isLoading: status === 'loading',
|
||||||
currentWorkspaceId,
|
currentWorkspaceId,
|
||||||
|
logOut,
|
||||||
updateUser,
|
updateUser,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const SignInForm = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === 'authenticated') {
|
if (status === 'authenticated') {
|
||||||
router.replace(router.query.callbackUrl?.toString() ?? '/typebots')
|
router.replace(router.query.redirectPath?.toString() ?? '/typebots')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
;(async () => {
|
;(async () => {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { useRouter } from 'next/router'
|
|||||||
import { useToast } from '../../../hooks/useToast'
|
import { useToast } from '../../../hooks/useToast'
|
||||||
import { Credentials } from '@typebot.io/schemas'
|
import { Credentials } from '@typebot.io/schemas'
|
||||||
import { trpc } from '@/lib/trpc'
|
import { trpc } from '@/lib/trpc'
|
||||||
|
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||||
|
|
||||||
type Props = Omit<ButtonProps, 'type'> & {
|
type Props = Omit<ButtonProps, 'type'> & {
|
||||||
type: Credentials['type']
|
type: Credentials['type']
|
||||||
@@ -38,6 +39,7 @@ export const CredentialsDropdown = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
|
const { currentRole } = useWorkspace()
|
||||||
const { data, refetch } = trpc.credentials.listCredentials.useQuery({
|
const { data, refetch } = trpc.credentials.listCredentials.useQuery({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
type,
|
type,
|
||||||
@@ -107,6 +109,7 @@ export const CredentialsDropdown = ({
|
|||||||
textAlign="left"
|
textAlign="left"
|
||||||
leftIcon={<PlusIcon />}
|
leftIcon={<PlusIcon />}
|
||||||
onClick={onCreateNewClick}
|
onClick={onCreateNewClick}
|
||||||
|
isDisabled={currentRole === 'GUEST'}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
Add {credentialsName}
|
Add {credentialsName}
|
||||||
@@ -165,16 +168,18 @@ export const CredentialsDropdown = ({
|
|||||||
/>
|
/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
<MenuItem
|
{currentRole === 'GUEST' ? null : (
|
||||||
maxW="500px"
|
<MenuItem
|
||||||
overflow="hidden"
|
maxW="500px"
|
||||||
whiteSpace="nowrap"
|
overflow="hidden"
|
||||||
textOverflow="ellipsis"
|
whiteSpace="nowrap"
|
||||||
icon={<PlusIcon />}
|
textOverflow="ellipsis"
|
||||||
onClick={onCreateNewClick}
|
icon={<PlusIcon />}
|
||||||
>
|
onClick={onCreateNewClick}
|
||||||
Connect new
|
>
|
||||||
</MenuItem>
|
Connect new
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { HStack, Flex, Button, useDisclosure } from '@chakra-ui/react'
|
import { HStack, Flex, Button, useDisclosure } from '@chakra-ui/react'
|
||||||
import { HardDriveIcon, SettingsIcon } from '@/components/icons'
|
import { HardDriveIcon, SettingsIcon } from '@/components/icons'
|
||||||
import { signOut } from 'next-auth/react'
|
|
||||||
import { useUser } from '@/features/account/hooks/useUser'
|
import { useUser } from '@/features/account/hooks/useUser'
|
||||||
import { isNotDefined } from '@typebot.io/lib'
|
import { isNotDefined } from '@typebot.io/lib'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
@@ -13,15 +12,11 @@ import { WorkspaceSettingsModal } from '@/features/workspace/components/Workspac
|
|||||||
|
|
||||||
export const DashboardHeader = () => {
|
export const DashboardHeader = () => {
|
||||||
const scopedT = useScopedI18n('dashboard.header')
|
const scopedT = useScopedI18n('dashboard.header')
|
||||||
const { user } = useUser()
|
const { user, logOut } = useUser()
|
||||||
const { workspace, switchWorkspace, createWorkspace } = useWorkspace()
|
const { workspace, switchWorkspace, createWorkspace } = useWorkspace()
|
||||||
|
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
|
|
||||||
const handleLogOut = () => {
|
|
||||||
signOut()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCreateNewWorkspace = () =>
|
const handleCreateNewWorkspace = () =>
|
||||||
createWorkspace(user?.name ?? undefined)
|
createWorkspace(user?.name ?? undefined)
|
||||||
|
|
||||||
@@ -59,7 +54,7 @@ export const DashboardHeader = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
<WorkspaceDropdown
|
<WorkspaceDropdown
|
||||||
currentWorkspace={workspace}
|
currentWorkspace={workspace}
|
||||||
onLogoutClick={handleLogOut}
|
onLogoutClick={logOut}
|
||||||
onCreateNewWorkspaceClick={handleCreateNewWorkspace}
|
onCreateNewWorkspaceClick={handleCreateNewWorkspace}
|
||||||
onWorkspaceSelected={switchWorkspace}
|
onWorkspaceSelected={switchWorkspace}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ export const TypebotProvider = ({
|
|||||||
|
|
||||||
const saveTypebot = useCallback(
|
const saveTypebot = useCallback(
|
||||||
async (updates?: Partial<Typebot>) => {
|
async (updates?: Partial<Typebot>) => {
|
||||||
if (!localTypebot || !typebot) return
|
if (!localTypebot || !typebot || typebotData?.isReadOnly) return
|
||||||
const typebotToSave = { ...localTypebot, ...updates }
|
const typebotToSave = { ...localTypebot, ...updates }
|
||||||
if (dequal(omit(typebot, 'updatedAt'), omit(typebotToSave, 'updatedAt')))
|
if (dequal(omit(typebot, 'updatedAt'), omit(typebotToSave, 'updatedAt')))
|
||||||
return
|
return
|
||||||
@@ -180,7 +180,13 @@ export const TypebotProvider = ({
|
|||||||
setLocalTypebot({ ...newTypebot })
|
setLocalTypebot({ ...newTypebot })
|
||||||
return newTypebot
|
return newTypebot
|
||||||
},
|
},
|
||||||
[localTypebot, setLocalTypebot, typebot, updateTypebot]
|
[
|
||||||
|
localTypebot,
|
||||||
|
setLocalTypebot,
|
||||||
|
typebot,
|
||||||
|
typebotData?.isReadOnly,
|
||||||
|
updateTypebot,
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
useAutoSave(
|
useAutoSave(
|
||||||
@@ -212,7 +218,7 @@ export const TypebotProvider = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!localTypebot || !typebot) return
|
if (!localTypebot || !typebot || typebotData?.isReadOnly) return
|
||||||
if (!areTypebotsEqual(localTypebot, typebot)) {
|
if (!areTypebotsEqual(localTypebot, typebot)) {
|
||||||
window.addEventListener('beforeunload', preventUserFromRefreshing)
|
window.addEventListener('beforeunload', preventUserFromRefreshing)
|
||||||
}
|
}
|
||||||
@@ -220,7 +226,7 @@ export const TypebotProvider = ({
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('beforeunload', preventUserFromRefreshing)
|
window.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||||
}
|
}
|
||||||
}, [localTypebot, typebot])
|
}, [localTypebot, typebot, typebotData?.isReadOnly])
|
||||||
|
|
||||||
const updateLocalTypebot = async ({
|
const updateLocalTypebot = async ({
|
||||||
updates,
|
updates,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { AlignLeftTextIcon } from '@/components/icons'
|
import { AlignLeftTextIcon } from '@/components/icons'
|
||||||
import { ResultHeaderCell, ResultsTablePreferences } from '@typebot.io/schemas'
|
import { ResultHeaderCell, ResultsTablePreferences } from '@typebot.io/schemas'
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { LoadingRows } from './LoadingRows'
|
import { LoadingRows } from './LoadingRows'
|
||||||
import {
|
import {
|
||||||
useReactTable,
|
useReactTable,
|
||||||
@@ -48,7 +48,7 @@ export const ResultsTable = ({
|
|||||||
onResultExpandIndex,
|
onResultExpandIndex,
|
||||||
}: ResultsTableProps) => {
|
}: ResultsTableProps) => {
|
||||||
const background = useColorModeValue('white', colors.gray[900])
|
const background = useColorModeValue('white', colors.gray[900])
|
||||||
const { updateTypebot } = useTypebot()
|
const { updateTypebot, isReadOnly } = useTypebot()
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({})
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({})
|
||||||
const [isTableScrolled, setIsTableScrolled] = useState(false)
|
const [isTableScrolled, setIsTableScrolled] = useState(false)
|
||||||
const bottomElement = useRef<HTMLDivElement | null>(null)
|
const bottomElement = useRef<HTMLDivElement | null>(null)
|
||||||
@@ -185,6 +185,14 @@ export const ResultsTable = ({
|
|||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleObserver = useCallback(
|
||||||
|
(entities: IntersectionObserverEntry[]) => {
|
||||||
|
const target = entities[0]
|
||||||
|
if (target.isIntersecting) onScrollToBottom()
|
||||||
|
},
|
||||||
|
[onScrollToBottom]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!bottomElement.current) return
|
if (!bottomElement.current) return
|
||||||
const options: IntersectionObserverInit = {
|
const options: IntersectionObserverInit = {
|
||||||
@@ -197,21 +205,17 @@ export const ResultsTable = ({
|
|||||||
return () => {
|
return () => {
|
||||||
observer.disconnect()
|
observer.disconnect()
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [handleObserver])
|
||||||
}, [bottomElement.current])
|
|
||||||
|
|
||||||
const handleObserver = (entities: IntersectionObserverEntry[]) => {
|
|
||||||
const target = entities[0]
|
|
||||||
if (target.isIntersecting) onScrollToBottom()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack maxW="1600px" px="4" overflowY="hidden" spacing={6}>
|
<Stack maxW="1600px" px="4" overflowY="hidden" spacing={6}>
|
||||||
<HStack w="full" justifyContent="flex-end">
|
<HStack w="full" justifyContent="flex-end">
|
||||||
<SelectionToolbar
|
{isReadOnly ? null : (
|
||||||
selectedResultsId={Object.keys(rowSelection)}
|
<SelectionToolbar
|
||||||
onClearSelection={() => setRowSelection({})}
|
selectedResultsId={Object.keys(rowSelection)}
|
||||||
/>
|
onClearSelection={() => setRowSelection({})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<TableSettingsButton
|
<TableSettingsButton
|
||||||
resultHeader={resultHeader}
|
resultHeader={resultHeader}
|
||||||
columnVisibility={columnsVisibility}
|
columnVisibility={columnsVisibility}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const WorkspaceProvider = ({
|
|||||||
typebotId,
|
typebotId,
|
||||||
children,
|
children,
|
||||||
}: WorkspaceContextProps) => {
|
}: WorkspaceContextProps) => {
|
||||||
const { pathname, query, push } = useRouter()
|
const { pathname, query, push, isReady: isRouterReady } = useRouter()
|
||||||
const { user } = useUser()
|
const { user } = useUser()
|
||||||
const userId = user?.id
|
const userId = user?.id
|
||||||
const [workspaceId, setWorkspaceId] = useState<string | undefined>()
|
const [workspaceId, setWorkspaceId] = useState<string | undefined>()
|
||||||
@@ -102,6 +102,8 @@ export const WorkspaceProvider = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
|
pathname === '/signin' ||
|
||||||
|
!isRouterReady ||
|
||||||
!workspaces ||
|
!workspaces ||
|
||||||
workspaces.length === 0 ||
|
workspaces.length === 0 ||
|
||||||
workspaceId ||
|
workspaceId ||
|
||||||
@@ -122,7 +124,9 @@ export const WorkspaceProvider = ({
|
|||||||
setWorkspaceIdInLocalStorage(newWorkspaceId)
|
setWorkspaceIdInLocalStorage(newWorkspaceId)
|
||||||
setWorkspaceId(newWorkspaceId)
|
setWorkspaceId(newWorkspaceId)
|
||||||
}, [
|
}, [
|
||||||
|
isRouterReady,
|
||||||
members,
|
members,
|
||||||
|
pathname,
|
||||||
query.workspaceId,
|
query.workspaceId,
|
||||||
typebot?.workspaceId,
|
typebot?.workspaceId,
|
||||||
typebotId,
|
typebotId,
|
||||||
|
|||||||
Reference in New Issue
Block a user