🚸 (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()
|
||||
}
|
||||
if (!publishedTypebot) {
|
||||
const newPublicId = parseDefaultPublicId(
|
||||
localTypebot.name,
|
||||
localTypebot.id
|
||||
)
|
||||
const newPublicId =
|
||||
localTypebot.publicId ??
|
||||
parseDefaultPublicId(localTypebot.name, localTypebot.id)
|
||||
updateLocalTypebot({ publicId: newPublicId, publishedTypebotId })
|
||||
newLocalTypebot.publicId = newPublicId
|
||||
await saveTypebot()
|
||||
|
@ -11,30 +11,27 @@ import {
|
||||
} from '@chakra-ui/react'
|
||||
import { EditIcon } from '@/components/icons'
|
||||
import { CopyButton } from '@/components/CopyButton'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
type EditableUrlProps = {
|
||||
hostname: string
|
||||
pathname?: string
|
||||
isValid: (newPathname: string) => Promise<boolean> | boolean
|
||||
onPathnameChange: (pathname: string) => void
|
||||
}
|
||||
|
||||
export const EditableUrl = ({
|
||||
hostname,
|
||||
pathname,
|
||||
isValid,
|
||||
onPathnameChange,
|
||||
}: EditableUrlProps) => {
|
||||
const { showToast } = useToast()
|
||||
const [value, setValue] = useState(pathname)
|
||||
|
||||
const handleSubmit = (newPathname: string) => {
|
||||
if (/^[a-z0-9-]*$/.test(newPathname)) return onPathnameChange(newPathname)
|
||||
const handleSubmit = async (newPathname: string) => {
|
||||
if (newPathname === pathname) return
|
||||
if (await isValid(newPathname)) return onPathnameChange(newPathname)
|
||||
setValue(pathname)
|
||||
showToast({
|
||||
title: 'Invalid ID',
|
||||
description: 'Should contain only contain letters, numbers and dashes.',
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -10,6 +10,7 @@ import { CustomDomainsDropdown } from '@/features/customDomains'
|
||||
import { TypebotHeader, useTypebot } from '@/features/editor'
|
||||
import { useWorkspace } from '@/features/workspace'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { isCloudProdInstance } from '@/utils/helpers'
|
||||
import {
|
||||
Flex,
|
||||
Heading,
|
||||
@ -32,14 +33,6 @@ export const SharePage = () => {
|
||||
const { showToast } = useToast()
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
@ -59,6 +52,40 @@ export const SharePage = () => {
|
||||
const handleCustomDomainChange = (customDomain: string | undefined) =>
|
||||
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 (
|
||||
<Flex flexDir="column" pb="40">
|
||||
<Seo title="Share" />
|
||||
@ -75,6 +102,7 @@ export const SharePage = () => {
|
||||
getViewerUrl({ isBuilder: true }) ?? 'https://typebot.io'
|
||||
}
|
||||
pathname={publicId}
|
||||
isValid={checkIfPublicIdIsValid}
|
||||
onPathnameChange={handlePublicIdChange}
|
||||
/>
|
||||
)}
|
||||
@ -83,6 +111,7 @@ export const SharePage = () => {
|
||||
<EditableUrl
|
||||
hostname={'https://' + typebot.customDomain.split('/')[0]}
|
||||
pathname={typebot.customDomain.split('/')[1]}
|
||||
isValid={checkIfPathnameIsValid}
|
||||
onPathnameChange={handlePathnameChange}
|
||||
/>
|
||||
<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.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').press('Enter')
|
||||
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 { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { badRequest, methodNotAllowed, notAuthenticated } from 'utils/api'
|
||||
import { methodNotAllowed, notAuthenticated } from 'utils/api'
|
||||
import { getAuthenticatedUser } from '@/features/auth/api'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
@ -8,8 +8,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (!user) return notAuthenticated(res)
|
||||
if (req.method === 'GET') {
|
||||
const publicId = req.query.publicId as string | undefined
|
||||
if (!publicId) return badRequest(res, 'publicId is required')
|
||||
const exists = await prisma.typebot.count({ where: { publicId } })
|
||||
const exists = await prisma.typebot.count({
|
||||
where: { publicId: publicId ?? '' },
|
||||
})
|
||||
return res.send({ isAvailable: Boolean(!exists) })
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
|
@ -51,7 +51,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
})
|
||||
const typebots = await prisma.typebot.updateMany({
|
||||
where: canWriteTypebots(typebotId, user),
|
||||
data: { isArchived: true },
|
||||
data: { isArchived: true, publicId: null },
|
||||
})
|
||||
return res.send({ typebots })
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ export const canWriteTypebots = (
|
||||
user: Pick<User, 'email' | 'id'>
|
||||
): Prisma.TypebotWhereInput =>
|
||||
isNotEmpty(env('E2E_TEST'))
|
||||
? {}
|
||||
? { id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds } }
|
||||
: {
|
||||
id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds },
|
||||
OR: [
|
||||
|
@ -62,9 +62,8 @@ export const getServerSideProps: GetServerSideProps = async (
|
||||
const getTypebotFromPublicId = async (
|
||||
publicId?: string
|
||||
): Promise<TypebotPageProps['publishedTypebot'] | null> => {
|
||||
if (!publicId) return null
|
||||
const publishedTypebot = await prisma.publicTypebot.findFirst({
|
||||
where: { typebot: { publicId } },
|
||||
where: { typebot: { publicId: publicId ?? '' } },
|
||||
include: {
|
||||
typebot: { select: { name: true, isClosed: true, isArchived: true } },
|
||||
},
|
||||
|
@ -52,9 +52,8 @@ export const getServerSideProps: GetServerSideProps = async (
|
||||
const getTypebotFromPublicId = async (
|
||||
publicId?: string
|
||||
): Promise<TypebotPageV2Props['typebot'] | null> => {
|
||||
if (!publicId) return null
|
||||
const typebot = (await prisma.typebot.findUnique({
|
||||
where: { publicId },
|
||||
where: { publicId: publicId ?? '' },
|
||||
select: {
|
||||
theme: true,
|
||||
name: true,
|
||||
|
Reference in New Issue
Block a user