🚸 (publish) Improve invalid public ID feedback
Also remove the 4 char min length rule for self-hosted versions Closes #267
This commit is contained in:
@ -261,10 +261,9 @@ export const TypebotProvider = ({
|
|||||||
await saveTypebot()
|
await saveTypebot()
|
||||||
}
|
}
|
||||||
if (!publishedTypebot) {
|
if (!publishedTypebot) {
|
||||||
const newPublicId = parseDefaultPublicId(
|
const newPublicId =
|
||||||
localTypebot.name,
|
localTypebot.publicId ??
|
||||||
localTypebot.id
|
parseDefaultPublicId(localTypebot.name, localTypebot.id)
|
||||||
)
|
|
||||||
updateLocalTypebot({ publicId: newPublicId, publishedTypebotId })
|
updateLocalTypebot({ publicId: newPublicId, publishedTypebotId })
|
||||||
newLocalTypebot.publicId = newPublicId
|
newLocalTypebot.publicId = newPublicId
|
||||||
await saveTypebot()
|
await saveTypebot()
|
||||||
|
@ -11,30 +11,27 @@ import {
|
|||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { EditIcon } from '@/components/icons'
|
import { EditIcon } from '@/components/icons'
|
||||||
import { CopyButton } from '@/components/CopyButton'
|
import { CopyButton } from '@/components/CopyButton'
|
||||||
import { useToast } from '@/hooks/useToast'
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
type EditableUrlProps = {
|
type EditableUrlProps = {
|
||||||
hostname: string
|
hostname: string
|
||||||
pathname?: string
|
pathname?: string
|
||||||
|
isValid: (newPathname: string) => Promise<boolean> | boolean
|
||||||
onPathnameChange: (pathname: string) => void
|
onPathnameChange: (pathname: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditableUrl = ({
|
export const EditableUrl = ({
|
||||||
hostname,
|
hostname,
|
||||||
pathname,
|
pathname,
|
||||||
|
isValid,
|
||||||
onPathnameChange,
|
onPathnameChange,
|
||||||
}: EditableUrlProps) => {
|
}: EditableUrlProps) => {
|
||||||
const { showToast } = useToast()
|
|
||||||
const [value, setValue] = useState(pathname)
|
const [value, setValue] = useState(pathname)
|
||||||
|
|
||||||
const handleSubmit = (newPathname: string) => {
|
const handleSubmit = async (newPathname: string) => {
|
||||||
if (/^[a-z0-9-]*$/.test(newPathname)) return onPathnameChange(newPathname)
|
if (newPathname === pathname) return
|
||||||
|
if (await isValid(newPathname)) return onPathnameChange(newPathname)
|
||||||
setValue(pathname)
|
setValue(pathname)
|
||||||
showToast({
|
|
||||||
title: 'Invalid ID',
|
|
||||||
description: 'Should contain only contain letters, numbers and dashes.',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -10,6 +10,7 @@ import { CustomDomainsDropdown } from '@/features/customDomains'
|
|||||||
import { TypebotHeader, useTypebot } from '@/features/editor'
|
import { TypebotHeader, useTypebot } from '@/features/editor'
|
||||||
import { useWorkspace } from '@/features/workspace'
|
import { useWorkspace } from '@/features/workspace'
|
||||||
import { useToast } from '@/hooks/useToast'
|
import { useToast } from '@/hooks/useToast'
|
||||||
|
import { isCloudProdInstance } from '@/utils/helpers'
|
||||||
import {
|
import {
|
||||||
Flex,
|
Flex,
|
||||||
Heading,
|
Heading,
|
||||||
@ -32,14 +33,6 @@ export const SharePage = () => {
|
|||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
|
|
||||||
const handlePublicIdChange = async (publicId: string) => {
|
const handlePublicIdChange = async (publicId: string) => {
|
||||||
if (publicId === typebot?.publicId) return
|
|
||||||
if (publicId.length < 4)
|
|
||||||
return showToast({ description: 'ID must be longer than 4 characters' })
|
|
||||||
|
|
||||||
const { data } = await isPublicDomainAvailableQuery(publicId)
|
|
||||||
if (!data?.isAvailable)
|
|
||||||
return showToast({ description: 'ID is already taken' })
|
|
||||||
|
|
||||||
updateTypebot({ publicId })
|
updateTypebot({ publicId })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,6 +52,40 @@ export const SharePage = () => {
|
|||||||
const handleCustomDomainChange = (customDomain: string | undefined) =>
|
const handleCustomDomainChange = (customDomain: string | undefined) =>
|
||||||
updateTypebot({ customDomain })
|
updateTypebot({ customDomain })
|
||||||
|
|
||||||
|
const checkIfPathnameIsValid = (pathname: string) => {
|
||||||
|
const isCorrectlyFormatted =
|
||||||
|
/^([a-z0-9]+-[a-z0-9]+)*$/.test(pathname) || /^[a-z0-9]*$/.test(pathname)
|
||||||
|
|
||||||
|
if (!isCorrectlyFormatted) {
|
||||||
|
showToast({
|
||||||
|
description:
|
||||||
|
'Should contain only contain letters, numbers. Words can be separated by dashes.',
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkIfPublicIdIsValid = async (publicId: string) => {
|
||||||
|
const isLongerThanAllowed = publicId.length >= 4
|
||||||
|
if (!isLongerThanAllowed && isCloudProdInstance) {
|
||||||
|
showToast({
|
||||||
|
description: 'Should be longer than 4 characters',
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkIfPathnameIsValid(publicId)) return false
|
||||||
|
|
||||||
|
const { data } = await isPublicDomainAvailableQuery(publicId)
|
||||||
|
if (!data?.isAvailable) {
|
||||||
|
showToast({ description: 'ID is already taken' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex flexDir="column" pb="40">
|
<Flex flexDir="column" pb="40">
|
||||||
<Seo title="Share" />
|
<Seo title="Share" />
|
||||||
@ -75,6 +102,7 @@ export const SharePage = () => {
|
|||||||
getViewerUrl({ isBuilder: true }) ?? 'https://typebot.io'
|
getViewerUrl({ isBuilder: true }) ?? 'https://typebot.io'
|
||||||
}
|
}
|
||||||
pathname={publicId}
|
pathname={publicId}
|
||||||
|
isValid={checkIfPublicIdIsValid}
|
||||||
onPathnameChange={handlePublicIdChange}
|
onPathnameChange={handlePublicIdChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -83,6 +111,7 @@ export const SharePage = () => {
|
|||||||
<EditableUrl
|
<EditableUrl
|
||||||
hostname={'https://' + typebot.customDomain.split('/')[0]}
|
hostname={'https://' + typebot.customDomain.split('/')[0]}
|
||||||
pathname={typebot.customDomain.split('/')[1]}
|
pathname={typebot.customDomain.split('/')[1]}
|
||||||
|
isValid={checkIfPathnameIsValid}
|
||||||
onPathnameChange={handlePathnameChange}
|
onPathnameChange={handlePathnameChange}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -29,7 +29,22 @@ test('should not be able to submit taken url ID', async ({ page }) => {
|
|||||||
])
|
])
|
||||||
await page.goto(`/typebots/${typebotId}/share`)
|
await page.goto(`/typebots/${typebotId}/share`)
|
||||||
await page.getByText(`${typebotId}-public`).click()
|
await page.getByText(`${typebotId}-public`).click()
|
||||||
|
await page.getByRole('textbox').fill('id with spaces')
|
||||||
|
await page.getByRole('textbox').press('Enter')
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.getByText(
|
||||||
|
'Should contain only contain letters, numbers. Words can be separated by dashes.'
|
||||||
|
)
|
||||||
|
.nth(0)
|
||||||
|
).toBeVisible()
|
||||||
|
await page.getByText(`${typebotId}-public`).click()
|
||||||
await page.getByRole('textbox').fill('taken-url-id')
|
await page.getByRole('textbox').fill('taken-url-id')
|
||||||
await page.getByRole('textbox').press('Enter')
|
await page.getByRole('textbox').press('Enter')
|
||||||
await expect(page.getByText('ID is already taken').nth(0)).toBeVisible()
|
await expect(page.getByText('ID is already taken').nth(0)).toBeVisible()
|
||||||
|
await page.getByText(`${typebotId}-public`).click()
|
||||||
|
await page.getByRole('textbox').fill('new-valid-id')
|
||||||
|
await page.getByRole('textbox').press('Enter')
|
||||||
|
await expect(page.getByText('new-valid-id')).toBeVisible()
|
||||||
|
await expect(page.getByText(`${typebotId}-public`)).toBeHidden()
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { badRequest, methodNotAllowed, notAuthenticated } from 'utils/api'
|
import { methodNotAllowed, notAuthenticated } from 'utils/api'
|
||||||
import { getAuthenticatedUser } from '@/features/auth/api'
|
import { getAuthenticatedUser } from '@/features/auth/api'
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
@ -8,8 +8,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
if (!user) return notAuthenticated(res)
|
if (!user) return notAuthenticated(res)
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
const publicId = req.query.publicId as string | undefined
|
const publicId = req.query.publicId as string | undefined
|
||||||
if (!publicId) return badRequest(res, 'publicId is required')
|
const exists = await prisma.typebot.count({
|
||||||
const exists = await prisma.typebot.count({ where: { publicId } })
|
where: { publicId: publicId ?? '' },
|
||||||
|
})
|
||||||
return res.send({ isAvailable: Boolean(!exists) })
|
return res.send({ isAvailable: Boolean(!exists) })
|
||||||
}
|
}
|
||||||
return methodNotAllowed(res)
|
return methodNotAllowed(res)
|
||||||
|
@ -51,7 +51,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
})
|
})
|
||||||
const typebots = await prisma.typebot.updateMany({
|
const typebots = await prisma.typebot.updateMany({
|
||||||
where: canWriteTypebots(typebotId, user),
|
where: canWriteTypebots(typebotId, user),
|
||||||
data: { isArchived: true },
|
data: { isArchived: true, publicId: null },
|
||||||
})
|
})
|
||||||
return res.send({ typebots })
|
return res.send({ typebots })
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ export const canWriteTypebots = (
|
|||||||
user: Pick<User, 'email' | 'id'>
|
user: Pick<User, 'email' | 'id'>
|
||||||
): Prisma.TypebotWhereInput =>
|
): Prisma.TypebotWhereInput =>
|
||||||
isNotEmpty(env('E2E_TEST'))
|
isNotEmpty(env('E2E_TEST'))
|
||||||
? {}
|
? { id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds } }
|
||||||
: {
|
: {
|
||||||
id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds },
|
id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds },
|
||||||
OR: [
|
OR: [
|
||||||
|
@ -62,9 +62,8 @@ export const getServerSideProps: GetServerSideProps = async (
|
|||||||
const getTypebotFromPublicId = async (
|
const getTypebotFromPublicId = async (
|
||||||
publicId?: string
|
publicId?: string
|
||||||
): Promise<TypebotPageProps['publishedTypebot'] | null> => {
|
): Promise<TypebotPageProps['publishedTypebot'] | null> => {
|
||||||
if (!publicId) return null
|
|
||||||
const publishedTypebot = await prisma.publicTypebot.findFirst({
|
const publishedTypebot = await prisma.publicTypebot.findFirst({
|
||||||
where: { typebot: { publicId } },
|
where: { typebot: { publicId: publicId ?? '' } },
|
||||||
include: {
|
include: {
|
||||||
typebot: { select: { name: true, isClosed: true, isArchived: true } },
|
typebot: { select: { name: true, isClosed: true, isArchived: true } },
|
||||||
},
|
},
|
||||||
|
@ -52,9 +52,8 @@ export const getServerSideProps: GetServerSideProps = async (
|
|||||||
const getTypebotFromPublicId = async (
|
const getTypebotFromPublicId = async (
|
||||||
publicId?: string
|
publicId?: string
|
||||||
): Promise<TypebotPageV2Props['typebot'] | null> => {
|
): Promise<TypebotPageV2Props['typebot'] | null> => {
|
||||||
if (!publicId) return null
|
|
||||||
const typebot = (await prisma.typebot.findUnique({
|
const typebot = (await prisma.typebot.findUnique({
|
||||||
where: { publicId },
|
where: { publicId: publicId ?? '' },
|
||||||
select: {
|
select: {
|
||||||
theme: true,
|
theme: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
Reference in New Issue
Block a user