🚸 (editor) Add graph gesture notification
This commit is contained in:
33
apps/builder/src/components/Toaster.tsx
Normal file
33
apps/builder/src/components/Toaster.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { colors } from '@/lib/theme'
|
||||||
|
import { useColorMode, useColorModeValue } from '@chakra-ui/react'
|
||||||
|
import { Toaster as SonnerToaster } from 'sonner'
|
||||||
|
|
||||||
|
export const Toaster = () => {
|
||||||
|
const { colorMode } = useColorMode()
|
||||||
|
const theme = useColorModeValue(
|
||||||
|
{
|
||||||
|
bg: undefined,
|
||||||
|
actionBg: colors.blue[500],
|
||||||
|
actionColor: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bg: colors.gray[900],
|
||||||
|
actionBg: colors.blue[400],
|
||||||
|
actionColor: 'white',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<SonnerToaster
|
||||||
|
theme={colorMode}
|
||||||
|
toastOptions={{
|
||||||
|
actionButtonStyle: {
|
||||||
|
backgroundColor: theme.actionBg,
|
||||||
|
color: theme.actionColor,
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
backgroundColor: theme.bg,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -643,3 +643,11 @@ export const XCircleIcon = (props: IconProps) => (
|
|||||||
<path d="m9 9 6 6" />
|
<path d="m9 9 6 6" />
|
||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const LightBulbIcon = (props: IconProps) => (
|
||||||
|
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||||
|
<path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5" />
|
||||||
|
<path d="M9 18h6" />
|
||||||
|
<path d="M10 22h4" />
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ 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'
|
||||||
import { User } from '@typebot.io/prisma'
|
import { User } from '@typebot.io/schemas'
|
||||||
import { setUser as setSentryUser } from '@sentry/nextjs'
|
import { setUser as setSentryUser } from '@sentry/nextjs'
|
||||||
import { useToast } from '@/hooks/useToast'
|
import { useToast } from '@/hooks/useToast'
|
||||||
import { updateUserQuery } from './queries/updateUserQuery'
|
import { updateUserQuery } from './queries/updateUserQuery'
|
||||||
|
|||||||
@@ -17,25 +17,17 @@ import {
|
|||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
} from '@/components/icons'
|
} from '@/components/icons'
|
||||||
import { useTypebot } from '../providers/TypebotProvider'
|
import { useTypebot } from '../providers/TypebotProvider'
|
||||||
import { useUser } from '@/features/account/hooks/useUser'
|
import React, { useState } from 'react'
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { EditorSettingsModal } from './EditorSettingsModal'
|
import { EditorSettingsModal } from './EditorSettingsModal'
|
||||||
import { parseDefaultPublicId } from '@/features/publish/helpers/parseDefaultPublicId'
|
import { parseDefaultPublicId } from '@/features/publish/helpers/parseDefaultPublicId'
|
||||||
import { useTranslate } from '@tolgee/react'
|
import { useTranslate } from '@tolgee/react'
|
||||||
|
|
||||||
export const BoardMenuButton = (props: FlexProps) => {
|
export const BoardMenuButton = (props: FlexProps) => {
|
||||||
const { query } = useRouter()
|
|
||||||
const { typebot } = useTypebot()
|
const { typebot } = useTypebot()
|
||||||
const { user } = useUser()
|
|
||||||
const [isDownloading, setIsDownloading] = useState(false)
|
const [isDownloading, setIsDownloading] = useState(false)
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
const { t } = useTranslate()
|
const { t } = useTranslate()
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user && !user.graphNavigation && !query.isFirstBot) onOpen()
|
|
||||||
}, [onOpen, query.isFirstBot, user, user?.graphNavigation])
|
|
||||||
|
|
||||||
const downloadFlow = () => {
|
const downloadFlow = () => {
|
||||||
assert(typebot)
|
assert(typebot)
|
||||||
setIsDownloading(true)
|
setIsDownloading(true)
|
||||||
@@ -54,7 +46,7 @@ export const BoardMenuButton = (props: FlexProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const redirectToDocumentation = () =>
|
const redirectToDocumentation = () =>
|
||||||
window.open('https://docs.typebot.io/get-started/overview', '_blank')
|
window.open('https://docs.typebot.io/editor/graph', '_blank')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ import { useGroupsStore } from '../hooks/useGroupsStore'
|
|||||||
import { useShallow } from 'zustand/react/shallow'
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
import { projectMouse } from '../helpers/projectMouse'
|
import { projectMouse } from '../helpers/projectMouse'
|
||||||
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts'
|
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts'
|
||||||
|
import { useUser } from '@/features/account/hooks/useUser'
|
||||||
|
import { graphGestureNotficationKey } from '@typebot.io/schemas/features/user/constants'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { LightBulbIcon } from '@/components/icons'
|
||||||
|
|
||||||
const maxScale = 2
|
const maxScale = 2
|
||||||
const minScale = 0.3
|
const minScale = 0.3
|
||||||
@@ -55,6 +59,7 @@ export const Graph = ({
|
|||||||
setDraggedItem,
|
setDraggedItem,
|
||||||
} = useBlockDnd()
|
} = useBlockDnd()
|
||||||
const { pasteGroups, createGroup } = useTypebot()
|
const { pasteGroups, createGroup } = useTypebot()
|
||||||
|
const { user, updateUser } = useUser()
|
||||||
const {
|
const {
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
setGraphPosition: setGlobalGraphPosition,
|
setGraphPosition: setGlobalGraphPosition,
|
||||||
@@ -184,6 +189,26 @@ export const Graph = ({
|
|||||||
setLastMouseClickPosition(
|
setLastMouseClickPosition(
|
||||||
projectMouse({ x: e.clientX, y: e.clientY }, graphPosition)
|
projectMouse({ x: e.clientX, y: e.clientY }, graphPosition)
|
||||||
)
|
)
|
||||||
|
} else if (
|
||||||
|
user &&
|
||||||
|
!user?.displayedInAppNotifications?.[graphGestureNotficationKey]
|
||||||
|
) {
|
||||||
|
toast.info('To move the graph using your mouse, hold the Space bar', {
|
||||||
|
action: {
|
||||||
|
label: 'More info',
|
||||||
|
onClick: () => {
|
||||||
|
window.open('https://docs.typebot.io/editor/graph', '_blank')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
duration: 30000,
|
||||||
|
icon: <LightBulbIcon w="20px" h="20px" />,
|
||||||
|
})
|
||||||
|
updateUser({
|
||||||
|
displayedInAppNotifications: {
|
||||||
|
...user.displayedInAppNotifications,
|
||||||
|
[graphGestureNotficationKey]: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
setSelectBoxCoordinates(undefined)
|
setSelectBoxCoordinates(undefined)
|
||||||
setOpenedBlockId(undefined)
|
setOpenedBlockId(undefined)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
|
|||||||
import { initPostHogIfEnabled } from '@/features/telemetry/posthog'
|
import { initPostHogIfEnabled } from '@/features/telemetry/posthog'
|
||||||
import { TolgeeProvider, useTolgeeSSR } from '@tolgee/react'
|
import { TolgeeProvider, useTolgeeSSR } from '@tolgee/react'
|
||||||
import { tolgee } from '@/lib/tolgee'
|
import { tolgee } from '@/lib/tolgee'
|
||||||
import { Toaster } from 'sonner'
|
import { Toaster } from '@/components/Toaster'
|
||||||
|
|
||||||
initPostHogIfEnabled()
|
initPostHogIfEnabled()
|
||||||
|
|
||||||
@@ -63,8 +63,8 @@ const App = ({ Component, pageProps }: AppProps) => {
|
|||||||
return (
|
return (
|
||||||
<TolgeeProvider tolgee={ssrTolgee}>
|
<TolgeeProvider tolgee={ssrTolgee}>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<Toaster />
|
|
||||||
<ChakraProvider theme={customTheme}>
|
<ChakraProvider theme={customTheme}>
|
||||||
|
<Toaster />
|
||||||
<SessionProvider session={pageProps.session}>
|
<SessionProvider session={pageProps.session}>
|
||||||
<UserProvider>
|
<UserProvider>
|
||||||
<TypebotProvider typebotId={typebotId}>
|
<TypebotProvider typebotId={typebotId}>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import prisma from '@typebot.io/lib/prisma'
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
|
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
|
||||||
import { methodNotAllowed, notAuthenticated } from '@typebot.io/lib/api'
|
import { methodNotAllowed, notAuthenticated } from '@typebot.io/lib/api'
|
||||||
import { User } from '@typebot.io/prisma'
|
import { Prisma, User } from '@typebot.io/prisma'
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
const user = await getAuthenticatedUser(req, res)
|
const user = await getAuthenticatedUser(req, res)
|
||||||
@@ -18,6 +18,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
onboardingCategories: data.onboardingCategories ?? [],
|
onboardingCategories: data.onboardingCategories ?? [],
|
||||||
|
displayedInAppNotifications:
|
||||||
|
data.displayedInAppNotifications ?? Prisma.DbNull,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return res.send({ typebots })
|
return res.send({ typebots })
|
||||||
|
|||||||
@@ -64,15 +64,6 @@
|
|||||||
"group": "Get Started",
|
"group": "Get Started",
|
||||||
"pages": ["get-started/introduction", "get-started/overview"]
|
"pages": ["get-started/introduction", "get-started/overview"]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"group": "Workspace",
|
|
||||||
"pages": [
|
|
||||||
"workspace",
|
|
||||||
"workspace/add-members-and-guests",
|
|
||||||
"workspace/subscription",
|
|
||||||
"workspace/preferences"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"group": "Flow",
|
"group": "Flow",
|
||||||
"pages": [
|
"pages": [
|
||||||
@@ -191,6 +182,15 @@
|
|||||||
"group": "Results",
|
"group": "Results",
|
||||||
"pages": ["results/overview", "results/analytics"]
|
"pages": ["results/overview", "results/analytics"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"group": "Workspace",
|
||||||
|
"pages": [
|
||||||
|
"workspace",
|
||||||
|
"workspace/add-members-and-guests",
|
||||||
|
"workspace/subscription",
|
||||||
|
"workspace/preferences"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"group": "Guides",
|
"group": "Guides",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { User } from '@typebot.io/prisma'
|
import { User } from '@typebot.io/prisma'
|
||||||
|
import { graphGestureNotficationKey } from '@typebot.io/schemas/features/user/constants'
|
||||||
|
|
||||||
export const mockedUser: User = {
|
export const mockedUser: User = {
|
||||||
id: 'userId',
|
id: 'userId',
|
||||||
@@ -13,4 +14,7 @@ export const mockedUser: User = {
|
|||||||
lastActivityAt: new Date('2022-01-01'),
|
lastActivityAt: new Date('2022-01-01'),
|
||||||
onboardingCategories: [],
|
onboardingCategories: [],
|
||||||
updatedAt: new Date('2022-01-01'),
|
updatedAt: new Date('2022-01-01'),
|
||||||
|
displayedInAppNotifications: {
|
||||||
|
[graphGestureNotficationKey]: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,8 @@ export const updateUser = (data: Partial<User>) =>
|
|||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
onboardingCategories: data.onboardingCategories ?? [],
|
onboardingCategories: data.onboardingCategories ?? [],
|
||||||
|
displayedInAppNotifications:
|
||||||
|
data.displayedInAppNotifications ?? Prisma.DbNull,
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
|
|||||||
@@ -41,24 +41,25 @@ model Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
lastActivityAt DateTime @default(now())
|
lastActivityAt DateTime @default(now())
|
||||||
name String? @db.VarChar(255)
|
name String? @db.VarChar(255)
|
||||||
email String? @unique
|
email String? @unique
|
||||||
emailVerified DateTime?
|
emailVerified DateTime?
|
||||||
image String? @db.VarChar(1000)
|
image String? @db.VarChar(1000)
|
||||||
company String?
|
company String?
|
||||||
onboardingCategories Json
|
onboardingCategories Json
|
||||||
graphNavigation GraphNavigation?
|
graphNavigation GraphNavigation?
|
||||||
preferredAppAppearance String?
|
preferredAppAppearance String?
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
apiTokens ApiToken[]
|
apiTokens ApiToken[]
|
||||||
CollaboratorsOnTypebots CollaboratorsOnTypebots[]
|
CollaboratorsOnTypebots CollaboratorsOnTypebots[]
|
||||||
workspaces MemberInWorkspace[]
|
workspaces MemberInWorkspace[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
bannedIps BannedIp[]
|
bannedIps BannedIp[]
|
||||||
|
displayedInAppNotifications Json?
|
||||||
}
|
}
|
||||||
|
|
||||||
model ApiToken {
|
model ApiToken {
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "displayedInAppNotifications" JSONB;
|
||||||
@@ -37,24 +37,25 @@ model Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
lastActivityAt DateTime @default(now())
|
lastActivityAt DateTime @default(now())
|
||||||
name String?
|
name String?
|
||||||
email String? @unique
|
email String? @unique
|
||||||
emailVerified DateTime?
|
emailVerified DateTime?
|
||||||
image String?
|
image String?
|
||||||
company String?
|
company String?
|
||||||
onboardingCategories Json
|
onboardingCategories Json
|
||||||
graphNavigation GraphNavigation?
|
graphNavigation GraphNavigation?
|
||||||
preferredAppAppearance String?
|
preferredAppAppearance String?
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
apiTokens ApiToken[]
|
apiTokens ApiToken[]
|
||||||
CollaboratorsOnTypebots CollaboratorsOnTypebots[]
|
CollaboratorsOnTypebots CollaboratorsOnTypebots[]
|
||||||
workspaces MemberInWorkspace[]
|
workspaces MemberInWorkspace[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
bannedIps BannedIp[]
|
bannedIps BannedIp[]
|
||||||
|
displayedInAppNotifications Json?
|
||||||
}
|
}
|
||||||
|
|
||||||
model ApiToken {
|
model ApiToken {
|
||||||
|
|||||||
1
packages/schemas/features/user/constants.ts
Normal file
1
packages/schemas/features/user/constants.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const graphGestureNotficationKey = 'graphGestureNotification'
|
||||||
22
packages/schemas/features/user/schema.ts
Normal file
22
packages/schemas/features/user/schema.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { GraphNavigation, User as PrismaUser } from '@typebot.io/prisma'
|
||||||
|
import { z } from '../../zod'
|
||||||
|
|
||||||
|
const displayedInAppNotificationsSchema = z.record(z.boolean())
|
||||||
|
|
||||||
|
export const userSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
lastActivityAt: z.date(),
|
||||||
|
name: z.string().nullable(),
|
||||||
|
email: z.string().nullable(),
|
||||||
|
emailVerified: z.date().nullable(),
|
||||||
|
image: z.string().nullable(),
|
||||||
|
company: z.string().nullable(),
|
||||||
|
onboardingCategories: z.array(z.string()),
|
||||||
|
graphNavigation: z.nativeEnum(GraphNavigation),
|
||||||
|
preferredAppAppearance: z.string().nullable(),
|
||||||
|
displayedInAppNotifications: displayedInAppNotificationsSchema.nullable(),
|
||||||
|
}) satisfies z.ZodType<PrismaUser>
|
||||||
|
|
||||||
|
export type User = z.infer<typeof userSchema>
|
||||||
@@ -13,3 +13,4 @@ export * from './features/workspace'
|
|||||||
export * from './features/items'
|
export * from './features/items'
|
||||||
export * from './features/analytics'
|
export * from './features/analytics'
|
||||||
export * from './features/events'
|
export * from './features/events'
|
||||||
|
export * from './features/user/schema'
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const inspectUser = async () => {
|
|||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
name: true,
|
name: true,
|
||||||
|
createdAt: true,
|
||||||
lastActivityAt: true,
|
lastActivityAt: true,
|
||||||
company: true,
|
company: true,
|
||||||
onboardingCategories: true,
|
onboardingCategories: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user