✨ Allow user to share a flow publicly and make it duplicatable
Closes #360
This commit is contained in:
@@ -117,7 +117,7 @@
|
|||||||
"eslint-config-custom": "workspace:*",
|
"eslint-config-custom": "workspace:*",
|
||||||
"next-runtime-env": "1.6.2",
|
"next-runtime-env": "1.6.2",
|
||||||
"superjson": "1.12.4",
|
"superjson": "1.12.4",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.3.2",
|
||||||
"zod": "3.22.4"
|
"zod": "3.22.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,7 +143,7 @@
|
|||||||
"editor.blocks.start.text": "Start",
|
"editor.blocks.start.text": "Start",
|
||||||
"editor.editableTypebotName.tooltip.rename.label": "Rename",
|
"editor.editableTypebotName.tooltip.rename.label": "Rename",
|
||||||
"editor.gettingStartedModal.editorBasics.heading": "Editor Basics",
|
"editor.gettingStartedModal.editorBasics.heading": "Editor Basics",
|
||||||
"editor.gettingStartedModal.editorBasics.list.four.label": "Preview your bot by clicking the preview button on the top right",
|
"editor.gettingStartedModal.editorBasics.list.four.label": "Preview your bot by clicking the test button on the top right",
|
||||||
"editor.gettingStartedModal.editorBasics.list.label": "Feel free to use the bottom-right bubble to reach out if you have any question. I usually answer within the next 24 hours. \uD83D\uDE03",
|
"editor.gettingStartedModal.editorBasics.list.label": "Feel free to use the bottom-right bubble to reach out if you have any question. I usually answer within the next 24 hours. \uD83D\uDE03",
|
||||||
"editor.gettingStartedModal.editorBasics.list.one.label": "The left side bar contains blocks that you can drag and drop to the board.",
|
"editor.gettingStartedModal.editorBasics.list.one.label": "The left side bar contains blocks that you can drag and drop to the board.",
|
||||||
"editor.gettingStartedModal.editorBasics.list.three.label": "Connect the groups together",
|
"editor.gettingStartedModal.editorBasics.list.three.label": "Connect the groups together",
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
"editor.gettingStartedModal.seeAction.time": "5 minutes",
|
"editor.gettingStartedModal.seeAction.time": "5 minutes",
|
||||||
"editor.headers.flowButton.label": "Flow",
|
"editor.headers.flowButton.label": "Flow",
|
||||||
"editor.headers.helpButton.label": "Help",
|
"editor.headers.helpButton.label": "Help",
|
||||||
"editor.headers.previewButton.label": "Preview",
|
"editor.headers.previewButton.label": "Test",
|
||||||
"editor.headers.resultsButton.label": "Results",
|
"editor.headers.resultsButton.label": "Results",
|
||||||
"editor.headers.savingSpinner.label": "Saving...",
|
"editor.headers.savingSpinner.label": "Saving...",
|
||||||
"editor.headers.settingsButton.label": "Settings",
|
"editor.headers.settingsButton.label": "Settings",
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { ButtonProps, Button, useClipboard } from '@chakra-ui/react'
|
|||||||
interface CopyButtonProps extends ButtonProps {
|
interface CopyButtonProps extends ButtonProps {
|
||||||
textToCopy: string
|
textToCopy: string
|
||||||
onCopied?: () => void
|
onCopied?: () => void
|
||||||
|
text?: {
|
||||||
|
copy: string
|
||||||
|
copied: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CopyButton = (props: CopyButtonProps) => {
|
export const CopyButton = (props: CopyButtonProps) => {
|
||||||
@@ -23,7 +27,7 @@ export const CopyButton = (props: CopyButtonProps) => {
|
|||||||
}}
|
}}
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
>
|
>
|
||||||
{!hasCopied ? 'Copy' : 'Copied'}
|
{!hasCopied ? props.text?.copy ?? 'Copy' : props.text?.copied ?? 'Copied'}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,8 +69,12 @@ export const UserProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!router.isReady) return
|
if (!router.isReady) return
|
||||||
if (status === 'loading') return
|
if (status === 'loading') return
|
||||||
const isSigningIn = () => ['/signin', '/register'].includes(router.pathname)
|
const isSignInPath = ['/signin', '/register'].includes(router.pathname)
|
||||||
if (!user && status === 'unauthenticated' && !isSigningIn())
|
const isPathPublicFriendly = /\/typebots\/.+\/(edit|theme|settings)/.test(
|
||||||
|
router.pathname
|
||||||
|
)
|
||||||
|
if (isSignInPath || isPathPublicFriendly) return
|
||||||
|
if (!user && status === 'unauthenticated')
|
||||||
router.replace({
|
router.replace({
|
||||||
pathname: '/signin',
|
pathname: '/signin',
|
||||||
query: {
|
query: {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const getTotalAnswers = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/typebots/{typebotId}/analytics/totalAnswersInBlocks',
|
path: '/v1/typebots/{typebotId}/analytics/totalAnswersInBlocks',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'List total answers in blocks',
|
summary: 'List total answers in blocks',
|
||||||
tags: ['Analytics'],
|
tags: ['Analytics'],
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const getTotalVisitedEdges = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/typebots/{typebotId}/analytics/totalVisitedEdges',
|
path: '/v1/typebots/{typebotId}/analytics/totalVisitedEdges',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'List total edges used in results',
|
summary: 'List total edges used in results',
|
||||||
tags: ['Analytics'],
|
tags: ['Analytics'],
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const totalSteps = 5
|
|||||||
|
|
||||||
export const OnboardingPage = () => {
|
export const OnboardingPage = () => {
|
||||||
const { t } = useTranslate()
|
const { t } = useTranslate()
|
||||||
const { push, replace } = useRouter()
|
const { replace, query } = useRouter()
|
||||||
const confettiCanvaContainer = useRef<HTMLCanvasElement | null>(null)
|
const confettiCanvaContainer = useRef<HTMLCanvasElement | null>(null)
|
||||||
const confettiCanon = useRef<confetti.CreateTypes>()
|
const confettiCanon = useRef<confetti.CreateTypes>()
|
||||||
const { user, updateUser } = useUser()
|
const { user, updateUser } = useUser()
|
||||||
@@ -38,8 +38,8 @@ export const OnboardingPage = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user?.createdAt) return
|
if (!user?.createdAt) return
|
||||||
if (isNewUser === false || !env.NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID)
|
if (isNewUser === false || !env.NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID)
|
||||||
replace('/typebots')
|
replace({ pathname: '/typebots', query })
|
||||||
}, [isNewUser, replace, user?.createdAt])
|
}, [isNewUser, query, replace, user?.createdAt])
|
||||||
|
|
||||||
const initConfettis = () => {
|
const initConfettis = () => {
|
||||||
if (!confettiCanvaContainer.current || confettiCanon.current) return
|
if (!confettiCanvaContainer.current || confettiCanon.current) return
|
||||||
@@ -83,7 +83,7 @@ export const OnboardingPage = () => {
|
|||||||
right="5"
|
right="5"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => push('/typebots')}
|
onClick={() => replace({ pathname: '/typebots', query })}
|
||||||
>
|
>
|
||||||
{t('skip')}
|
{t('skip')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -95,7 +95,10 @@ export const OnboardingPage = () => {
|
|||||||
prefilledVariables={{ Name: user?.name, Email: user?.email }}
|
prefilledVariables={{ Name: user?.name, Email: user?.email }}
|
||||||
onEnd={() => {
|
onEnd={() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
push('/typebots/create', { query: { isFirstBot: true } })
|
replace({
|
||||||
|
pathname: '/typebots',
|
||||||
|
query: { ...query, isFirstBot: true },
|
||||||
|
})
|
||||||
}, 2000)
|
}, 2000)
|
||||||
}}
|
}}
|
||||||
onAnswer={updateUserInfo}
|
onAnswer={updateUserInfo}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const createCheckoutSession = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/billing/subscription/checkout',
|
path: '/v1/billing/subscription/checkout',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Create checkout session to create a new subscription',
|
summary: 'Create checkout session to create a new subscription',
|
||||||
tags: ['Billing'],
|
tags: ['Billing'],
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const createCustomCheckoutSession = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/billing/subscription/custom-checkout',
|
path: '/v1/billing/subscription/custom-checkout',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary:
|
summary:
|
||||||
'Create custom checkout session to make a workspace pay for a custom plan',
|
'Create custom checkout session to make a workspace pay for a custom plan',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const getBillingPortalUrl = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/billing/subscription/portal',
|
path: '/v1/billing/subscription/portal',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Get Stripe billing portal URL',
|
summary: 'Get Stripe billing portal URL',
|
||||||
tags: ['Billing'],
|
tags: ['Billing'],
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const getSubscription = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/billing/subscription',
|
path: '/v1/billing/subscription',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'List invoices',
|
summary: 'List invoices',
|
||||||
tags: ['Billing'],
|
tags: ['Billing'],
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const getUsage = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/billing/usage',
|
path: '/v1/billing/usage',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Get current plan usage',
|
summary: 'Get current plan usage',
|
||||||
tags: ['Billing'],
|
tags: ['Billing'],
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const listInvoices = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/billing/invoices',
|
path: '/v1/billing/invoices',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'List invoices',
|
summary: 'List invoices',
|
||||||
tags: ['Billing'],
|
tags: ['Billing'],
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const updateSubscription = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
path: '/billing/subscription',
|
path: '/v1/billing/subscription',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Update subscription',
|
summary: 'Update subscription',
|
||||||
tags: ['Billing'],
|
tags: ['Billing'],
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ test('should work as expected', async ({ page }) => {
|
|||||||
'gm'
|
'gm'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
await page.getByRole('button', { name: 'Preview', exact: true }).click()
|
await page.getByRole('button', { name: 'Test', exact: true }).click()
|
||||||
await expect(page.locator('audio')).toHaveAttribute(
|
await expect(page.locator('audio')).toHaveAttribute(
|
||||||
'src',
|
'src',
|
||||||
RegExp(
|
RegExp(
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ test.describe.parallel('Embed bubble block', () => {
|
|||||||
])
|
])
|
||||||
|
|
||||||
await page.goto(`/typebots/${typebotId}/edit`)
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await expect(page.locator('iframe#embed-bubble-content')).toHaveAttribute(
|
await expect(page.locator('iframe#embed-bubble-content')).toHaveAttribute(
|
||||||
'src',
|
'src',
|
||||||
siteSrc
|
siteSrc
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ test.describe.parallel('Image bubble block', () => {
|
|||||||
])
|
])
|
||||||
|
|
||||||
await page.goto(`/typebots/${typebotId}/edit`)
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await expect(page.locator('img')).toHaveAttribute('src', unsplashImageSrc)
|
await expect(page.locator('img')).toHaveAttribute('src', unsplashImageSrc)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ test.describe('Text bubble block', () => {
|
|||||||
await page.fill('[data-testid="variables-input"]', 'test')
|
await page.fill('[data-testid="variables-input"]', 'test')
|
||||||
await page.getByRole('menuitem', { name: 'Create test' }).click()
|
await page.getByRole('menuitem', { name: 'Create test' }).click()
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await expect(page.locator('span.slate-bold >> nth=0')).toHaveText(
|
await expect(page.locator('span.slate-bold >> nth=0')).toHaveText(
|
||||||
'Bold text'
|
'Bold text'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ test.describe.parallel('Video bubble block', () => {
|
|||||||
])
|
])
|
||||||
|
|
||||||
await page.goto(`/typebots/${typebotId}/edit`)
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await expect(page.locator('video').nth(1)).toHaveAttribute(
|
await expect(page.locator('video').nth(1)).toHaveAttribute(
|
||||||
'src',
|
'src',
|
||||||
videoSrc
|
videoSrc
|
||||||
@@ -75,7 +75,7 @@ test.describe.parallel('Video bubble block', () => {
|
|||||||
])
|
])
|
||||||
|
|
||||||
await page.goto(`/typebots/${typebotId}/edit`)
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await expect(page.locator('iframe').nth(1)).toHaveAttribute(
|
await expect(page.locator('iframe').nth(1)).toHaveAttribute(
|
||||||
'src',
|
'src',
|
||||||
'https://www.youtube.com/embed/dQw4w9WgXcQ'
|
'https://www.youtube.com/embed/dQw4w9WgXcQ'
|
||||||
@@ -99,7 +99,7 @@ test.describe.parallel('Video bubble block', () => {
|
|||||||
])
|
])
|
||||||
|
|
||||||
await page.goto(`/typebots/${typebotId}/edit`)
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await expect(page.locator('iframe').nth(1)).toHaveAttribute(
|
await expect(page.locator('iframe').nth(1)).toHaveAttribute(
|
||||||
'src',
|
'src',
|
||||||
'https://player.vimeo.com/video/649301125'
|
'https://player.vimeo.com/video/649301125'
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ test.describe.parallel('Buttons input block', () => {
|
|||||||
await page.click('text=Delete')
|
await page.click('text=Delete')
|
||||||
await expect(page.locator('text=Item 2')).toBeHidden()
|
await expect(page.locator('text=Item 2')).toBeHidden()
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await page.getByRole('button', { name: 'Item 3' }).click()
|
await page.getByRole('button', { name: 'Item 3' }).click()
|
||||||
await expect(page.getByRole('button', { name: 'Item 3' })).toBeHidden()
|
await expect(page.getByRole('button', { name: 'Item 3' })).toBeHidden()
|
||||||
await expect(page.getByTestId('guest-bubble')).toHaveText('Item 3')
|
await expect(page.getByTestId('guest-bubble')).toHaveText('Item 3')
|
||||||
@@ -57,7 +57,7 @@ test.describe.parallel('Buttons input block', () => {
|
|||||||
await page.fill('input[value="Click to edit"]', 'Item 2')
|
await page.fill('input[value="Click to edit"]', 'Item 2')
|
||||||
await page.press('input[value="Item 2"]', 'Enter')
|
await page.press('input[value="Item 2"]', 'Enter')
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
|
|
||||||
await page.getByRole('checkbox', { name: 'Item 3' }).click()
|
await page.getByRole('checkbox', { name: 'Item 3' }).click()
|
||||||
await page.getByRole('checkbox', { name: 'Item 1' }).click()
|
await page.getByRole('checkbox', { name: 'Item 1' }).click()
|
||||||
@@ -77,7 +77,7 @@ test('Variable buttons should work', async ({ page }) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
await page.goto(`/typebots/${typebotId}/edit`)
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await page.getByRole('button', { name: 'Variable item' }).click()
|
await page.getByRole('button', { name: 'Variable item' }).click()
|
||||||
await expect(page.getByTestId('guest-bubble')).toHaveText('Variable item')
|
await expect(page.getByTestId('guest-bubble')).toHaveText('Variable item')
|
||||||
await expect(page.locator('text=Ok great!')).toBeVisible()
|
await expect(page.locator('text=Ok great!')).toBeVisible()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ test.describe('Date input block', () => {
|
|||||||
|
|
||||||
await page.goto(`/typebots/${typebotId}/edit`)
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await expect(page.locator('[data-testid="from-date"]')).toHaveAttribute(
|
await expect(page.locator('[data-testid="from-date"]')).toHaveAttribute(
|
||||||
'type',
|
'type',
|
||||||
'date'
|
'date'
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ test.describe('Email input block', () => {
|
|||||||
|
|
||||||
await page.goto(`/typebots/${typebotId}/edit`)
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await expect(
|
await expect(
|
||||||
page.locator(
|
page.locator(
|
||||||
`input[placeholder="${defaultEmailInputOptions.labels.placeholder}"]`
|
`input[placeholder="${defaultEmailInputOptions.labels.placeholder}"]`
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ test('options should work', async ({ page }) => {
|
|||||||
|
|
||||||
await page.goto(`/typebots/${typebotId}/edit`)
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await expect(page.locator(`text=Click to upload`)).toBeVisible()
|
await expect(page.locator(`text=Click to upload`)).toBeVisible()
|
||||||
await expect(page.locator(`text="Skip"`)).toBeHidden()
|
await expect(page.locator(`text="Skip"`)).toBeHidden()
|
||||||
await page
|
await page
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ test.describe('Number input block', () => {
|
|||||||
|
|
||||||
await page.goto(`/typebots/${typebotId}/edit`)
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await expect(
|
await expect(
|
||||||
page.locator(
|
page.locator(
|
||||||
`input[placeholder="${defaultNumberInputOptions.labels.placeholder}"]`
|
`input[placeholder="${defaultNumberInputOptions.labels.placeholder}"]`
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ test.describe('Payment input block', () => {
|
|||||||
await page.fill('[placeholder="john@gmail.com"]', 'test@typebot.io')
|
await page.fill('[placeholder="john@gmail.com"]', 'test@typebot.io')
|
||||||
await expect(page.locator('text="Phone number:"')).toBeVisible()
|
await expect(page.locator('text="Phone number:"')).toBeVisible()
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await stripePaymentForm(page)
|
await stripePaymentForm(page)
|
||||||
.locator(`[placeholder="1234 1234 1234 1234"]`)
|
.locator(`[placeholder="1234 1234 1234 1234"]`)
|
||||||
.fill('4000000000000002')
|
.fill('4000000000000002')
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ test.describe('Phone input block', () => {
|
|||||||
|
|
||||||
await page.goto(`/typebots/${typebotId}/edit`)
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await expect(
|
await expect(
|
||||||
page.locator(
|
page.locator(
|
||||||
`input[placeholder="${defaultPhoneInputOptions.labels.placeholder}"]`
|
`input[placeholder="${defaultPhoneInputOptions.labels.placeholder}"]`
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ test.describe.parallel('Picture choice input block', () => {
|
|||||||
await page.getByPlaceholder('Paste the image link...').fill(thirdImageSrc)
|
await page.getByPlaceholder('Paste the image link...').fill(thirdImageSrc)
|
||||||
await page.getByLabel('Title:').fill('Third image')
|
await page.getByLabel('Title:').fill('Third image')
|
||||||
await page.getByLabel('Description:').fill('Third description')
|
await page.getByLabel('Description:').fill('Third description')
|
||||||
await page.getByRole('button', { name: 'Preview' }).click()
|
await page.getByRole('button', { name: 'Test' }).click()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', {
|
page.getByRole('button', {
|
||||||
name: 'First image First image First description',
|
name: 'First image First image First description',
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ test('options should work', async ({ page }) => {
|
|||||||
|
|
||||||
await page.goto(`/typebots/${typebotId}/edit`)
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await expect(page.locator(`text=Send`)).toBeHidden()
|
await expect(page.locator(`text=Send`)).toBeHidden()
|
||||||
await page.getByRole('button', { name: '8' }).click()
|
await page.getByRole('button', { name: '8' }).click()
|
||||||
await page.locator(`text=Send`).click()
|
await page.locator(`text=Send`).click()
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ test.describe.parallel('Text input block', () => {
|
|||||||
|
|
||||||
await page.goto(`/typebots/${typebotId}/edit`)
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await expect(
|
await expect(
|
||||||
page.locator(
|
page.locator(
|
||||||
`input[placeholder="${defaultTextInputOptions.labels.placeholder}"]`
|
`input[placeholder="${defaultTextInputOptions.labels.placeholder}"]`
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ test.describe('Url input block', () => {
|
|||||||
|
|
||||||
await page.goto(`/typebots/${typebotId}/edit`)
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await expect(
|
await expect(
|
||||||
page.locator(
|
page.locator(
|
||||||
`input[placeholder="${defaultUrlInputOptions.labels.placeholder}"]`
|
`input[placeholder="${defaultUrlInputOptions.labels.placeholder}"]`
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ test.describe('Chatwoot block', () => {
|
|||||||
await page.getByLabel('Email').fill('john@email.com')
|
await page.getByLabel('Email').fill('john@email.com')
|
||||||
await page.getByLabel('Avatar URL').fill('https://domain.com/avatar.png')
|
await page.getByLabel('Avatar URL').fill('https://domain.com/avatar.png')
|
||||||
await page.getByLabel('Phone number').fill('+33654347543')
|
await page.getByLabel('Phone number').fill('+33654347543')
|
||||||
await page.getByRole('button', { name: 'Preview', exact: true }).click()
|
await page.getByRole('button', { name: 'Test', exact: true }).click()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText('Chatwoot block is not supported in preview').nth(0)
|
page.getByText('Chatwoot block is not supported in preview').nth(0)
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ test.describe.parallel('Google sheets integration', () => {
|
|||||||
'Georges'
|
'Georges'
|
||||||
)
|
)
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await page
|
await page
|
||||||
.locator('typebot-standard')
|
.locator('typebot-standard')
|
||||||
.locator('input[placeholder="Type your email..."]')
|
.locator('input[placeholder="Type your email..."]')
|
||||||
@@ -76,7 +76,7 @@ test.describe.parallel('Google sheets integration', () => {
|
|||||||
'Last name'
|
'Last name'
|
||||||
)
|
)
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await page
|
await page
|
||||||
.locator('typebot-standard')
|
.locator('typebot-standard')
|
||||||
.locator('input[placeholder="Type your email..."]')
|
.locator('input[placeholder="Type your email..."]')
|
||||||
@@ -132,7 +132,7 @@ test.describe.parallel('Google sheets integration', () => {
|
|||||||
await page.getByRole('menuitem', { name: 'Last name' }).click()
|
await page.getByRole('menuitem', { name: 'Last name' }).click()
|
||||||
await createNewVar(page, 'Last name')
|
await createNewVar(page, 'Last name')
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await page
|
await page
|
||||||
.locator('typebot-standard')
|
.locator('typebot-standard')
|
||||||
.locator('input[placeholder="Type your email..."]')
|
.locator('input[placeholder="Type your email..."]')
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ test.describe('Pixel block', () => {
|
|||||||
await page.getByRole('button', { name: 'Select key' }).click()
|
await page.getByRole('button', { name: 'Select key' }).click()
|
||||||
await page.getByRole('menuitem', { name: 'currency' }).click()
|
await page.getByRole('menuitem', { name: 'currency' }).click()
|
||||||
await page.getByPlaceholder('Value').fill('USD')
|
await page.getByPlaceholder('Value').fill('USD')
|
||||||
await page.getByRole('button', { name: 'Preview' }).click()
|
await page.getByRole('button', { name: 'Test' }).click()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText('Pixel is not enabled in Preview mode').nth(1)
|
page.getByText('Pixel is not enabled in Preview mode').nth(1)
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ test.describe('Send email block', () => {
|
|||||||
await page.click('text="Custom content?"')
|
await page.click('text="Custom content?"')
|
||||||
await page.locator('textarea').fill('Here is my email')
|
await page.locator('textarea').fill('Here is my email')
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await page.locator('typebot-standard').locator('text=Go').click()
|
await page.locator('typebot-standard').locator('text=Go').click()
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('text=Emails are not sent in preview mode >> nth=0')
|
page.locator('text=Emails are not sent in preview mode >> nth=0')
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const getResultExample = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/typebots/{typebotId}/webhookBlocks/{blockId}/getResultExample',
|
path: '/v1/typebots/{typebotId}/webhookBlocks/{blockId}/getResultExample',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Get result example',
|
summary: 'Get result example',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const listWebhookBlocks = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/typebots/{typebotId}/webhookBlocks',
|
path: '/v1/typebots/{typebotId}/webhookBlocks',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'List webhook blocks',
|
summary: 'List webhook blocks',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const subscribeWebhook = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/typebots/{typebotId}/webhookBlocks/{blockId}/subscribe',
|
path: '/v1/typebots/{typebotId}/webhookBlocks/{blockId}/subscribe',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Subscribe to webhook block',
|
summary: 'Subscribe to webhook block',
|
||||||
tags: ['Webhook'],
|
tags: ['Webhook'],
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const unsubscribeWebhook = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/typebots/{typebotId}/webhookBlocks/{blockId}/unsubscribe',
|
path: '/v1/typebots/{typebotId}/webhookBlocks/{blockId}/unsubscribe',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Unsubscribe from webhook block',
|
summary: 'Unsubscribe from webhook block',
|
||||||
tags: ['Webhook'],
|
tags: ['Webhook'],
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const ProjectsDropdown = ({
|
|||||||
const { workspace } = useWorkspace()
|
const { workspace } = useWorkspace()
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
|
|
||||||
const { data } = trpc.zemanticAi.listProjects.useQuery(
|
const { data } = trpc.zemanticAI.listProjects.useQuery(
|
||||||
{
|
{
|
||||||
credentialsId,
|
credentialsId,
|
||||||
workspaceId: workspace?.id as string,
|
workspaceId: workspace?.id as string,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ test.describe('AB Test block', () => {
|
|||||||
await page.getByLabel('Percent of users to follow A:').fill('100')
|
await page.getByLabel('Percent of users to follow A:').fill('100')
|
||||||
await expect(page.getByText('A 100%')).toBeVisible()
|
await expect(page.getByText('A 100%')).toBeVisible()
|
||||||
await expect(page.getByText('B 0%')).toBeVisible()
|
await expect(page.getByText('B 0%')).toBeVisible()
|
||||||
await page.getByRole('button', { name: 'Preview' }).click()
|
await page.getByRole('button', { name: 'Test' }).click()
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('typebot-standard').getByText('How are you?')
|
page.locator('typebot-standard').getByText('How are you?')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ test.describe('Condition block', () => {
|
|||||||
await page.click('button:has-text("Greater than")', { force: true })
|
await page.click('button:has-text("Greater than")', { force: true })
|
||||||
await page.fill('input[placeholder="Type a number..."]', '20')
|
await page.fill('input[placeholder="Type a number..."]', '20')
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await page
|
await page
|
||||||
.locator('typebot-standard')
|
.locator('typebot-standard')
|
||||||
.locator('input[placeholder="Type a number..."]')
|
.locator('input[placeholder="Type a number..."]')
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ test('should work as expected', async ({ page }) => {
|
|||||||
await page.getByRole('menuitem', { name: 'Group #1' }).click()
|
await page.getByRole('menuitem', { name: 'Group #1' }).click()
|
||||||
await page.getByPlaceholder('Select a block').click()
|
await page.getByPlaceholder('Select a block').click()
|
||||||
await page.getByRole('menuitem', { name: 'Block #2' }).click()
|
await page.getByRole('menuitem', { name: 'Block #2' }).click()
|
||||||
await page.getByRole('button', { name: 'Preview' }).click()
|
await page.getByRole('button', { name: 'Test' }).click()
|
||||||
await page.getByPlaceholder('Type your answer...').fill('Hi there!')
|
await page.getByPlaceholder('Type your answer...').fill('Hi there!')
|
||||||
await page.getByRole('button', { name: 'Send' }).click()
|
await page.getByRole('button', { name: 'Send' }).click()
|
||||||
await expect(
|
await expect(
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ test.describe('Redirect block', () => {
|
|||||||
await page.click('text=Configure...')
|
await page.click('text=Configure...')
|
||||||
await page.fill('input[placeholder="Type a URL..."]', 'google.com')
|
await page.fill('input[placeholder="Type a URL..."]', 'google.com')
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await page.locator('typebot-standard').locator('text=Go to URL').click()
|
await page.locator('typebot-standard').locator('text=Go to URL').click()
|
||||||
await expect(page).toHaveURL('https://www.google.com')
|
await expect(page).toHaveURL('https://www.google.com')
|
||||||
await page.goBack()
|
await page.goBack()
|
||||||
@@ -26,7 +26,7 @@ test.describe('Redirect block', () => {
|
|||||||
await page.click('text=Redirect to google.com')
|
await page.click('text=Redirect to google.com')
|
||||||
await page.click('text=Open in new tab')
|
await page.click('text=Open in new tab')
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
const [newPage] = await Promise.all([
|
const [newPage] = await Promise.all([
|
||||||
context.waitForEvent('page'),
|
context.waitForEvent('page'),
|
||||||
page.locator('typebot-standard').locator('text=Go to URL').click(),
|
page.locator('typebot-standard').locator('text=Go to URL').click(),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ test.describe('Script block', () => {
|
|||||||
'window.location.href = "https://www.google.com"'
|
'window.location.href = "https://www.google.com"'
|
||||||
)
|
)
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await page.getByRole('button', { name: 'Trigger code' }).click()
|
await page.getByRole('button', { name: 'Trigger code' }).click()
|
||||||
await expect(page).toHaveURL('https://www.google.com')
|
await expect(page).toHaveURL('https://www.google.com')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ test.describe('Set variable block', () => {
|
|||||||
.getByRole('textbox')
|
.getByRole('textbox')
|
||||||
.fill('1000 + {{Total}}')
|
.fill('1000 + {{Total}}')
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await page
|
await page
|
||||||
.locator('typebot-standard')
|
.locator('typebot-standard')
|
||||||
.locator('input[placeholder="Type a number..."]')
|
.locator('input[placeholder="Type a number..."]')
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const getLinkedTypebots = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/typebots/{typebotId}/linkedTypebots',
|
path: '/v1/typebots/{typebotId}/linkedTypebots',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Get linked typebots',
|
summary: 'Get linked typebots',
|
||||||
tags: ['Typebot'],
|
tags: ['Typebot'],
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ test('should be configurable', async ({ page }) => {
|
|||||||
await page.click('input[placeholder="Select a group"]')
|
await page.click('input[placeholder="Select a group"]')
|
||||||
await page.click('text=Group #2')
|
await page.click('text=Group #2')
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('typebot-standard').locator('text=Second block')
|
page.locator('typebot-standard').locator('text=Second block')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
@@ -45,7 +45,7 @@ test('should be configurable', async ({ page }) => {
|
|||||||
await page.getByTestId('selected-item-label').nth(1).click({ force: true })
|
await page.getByTestId('selected-item-label').nth(1).click({ force: true })
|
||||||
await page.click('button >> text=Start')
|
await page.click('button >> text=Start')
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await page.locator('typebot-standard').locator('input').fill('Hello there!')
|
await page.locator('typebot-standard').locator('input').fill('Hello there!')
|
||||||
await page.locator('typebot-standard').locator('input').press('Enter')
|
await page.locator('typebot-standard').locator('input').press('Enter')
|
||||||
await expect(
|
await expect(
|
||||||
@@ -60,7 +60,7 @@ test('should be configurable', async ({ page }) => {
|
|||||||
await page.getByRole('textbox').nth(1).click()
|
await page.getByRole('textbox').nth(1).click()
|
||||||
await page.click('button >> text=Hello')
|
await page.click('button >> text=Hello')
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('typebot-standard').locator('text=Hello world')
|
page.locator('typebot-standard').locator('text=Hello world')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ test.describe('Wait block', () => {
|
|||||||
await page.click('text=Configure...')
|
await page.click('text=Configure...')
|
||||||
await page.getByRole('textbox', { name: 'Seconds to wait for:' }).fill('3')
|
await page.getByRole('textbox', { name: 'Seconds to wait for:' }).fill('3')
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Test')
|
||||||
await page.getByRole('button', { name: 'Wait now' }).click()
|
await page.getByRole('button', { name: 'Wait now' }).click()
|
||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(1000)
|
||||||
await expect(
|
await expect(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const getCollaborators = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/typebots/{typebotId}/collaborators',
|
path: '/v1/typebots/{typebotId}/collaborators',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Get collaborators',
|
summary: 'Get collaborators',
|
||||||
tags: ['Collaborators'],
|
tags: ['Collaborators'],
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ test.describe('Typebot owner', () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
await page.goto(`/typebots/${typebotId}/edit`)
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
await page.click('button[aria-label="Show collaboration menu"]')
|
await page.click('button[aria-label="Open share popover"]')
|
||||||
await expect(page.locator('text=Free user')).toBeHidden()
|
await expect(page.locator('text=Free user')).toBeHidden()
|
||||||
await page.fill(
|
await page.fill(
|
||||||
'input[placeholder="colleague@company.com"]',
|
'input[placeholder="colleague@company.com"]',
|
||||||
@@ -109,7 +109,7 @@ test.describe('Guest with read access', () => {
|
|||||||
await expect(page.locator('text=Another typebot')).toBeHidden()
|
await expect(page.locator('text=Another typebot')).toBeHidden()
|
||||||
await expect(page.locator('text=Guest folder')).toBeHidden()
|
await expect(page.locator('text=Guest folder')).toBeHidden()
|
||||||
await page.click('text=Guest typebot')
|
await page.click('text=Guest typebot')
|
||||||
await page.click('button[aria-label="Show collaboration menu"]')
|
await page.click('button[aria-label="Open share popover"]')
|
||||||
await page.click('text=Everyone at Guest workspace')
|
await page.click('text=Everyone at Guest workspace')
|
||||||
await expect(page.locator('text="Remove"')).toBeHidden()
|
await expect(page.locator('text="Remove"')).toBeHidden()
|
||||||
await expect(page.locator('text=John Doe')).toBeVisible()
|
await expect(page.locator('text=John Doe')).toBeVisible()
|
||||||
@@ -165,11 +165,42 @@ test.describe('Guest with write access', () => {
|
|||||||
await expect(page.locator('text=Another typebot')).toBeHidden()
|
await expect(page.locator('text=Another typebot')).toBeHidden()
|
||||||
await expect(page.locator('text=Guest folder')).toBeHidden()
|
await expect(page.locator('text=Guest folder')).toBeHidden()
|
||||||
await page.click('text=Guest typebot')
|
await page.click('text=Guest typebot')
|
||||||
await page.click('button[aria-label="Show collaboration menu"]')
|
await page.click('button[aria-label="Open share popover"]')
|
||||||
await page.click('text=Everyone at Guest workspace')
|
await page.click('text=Everyone at Guest workspace')
|
||||||
await expect(page.locator('text="Remove"')).toBeHidden()
|
await expect(page.locator('text="Remove"')).toBeHidden()
|
||||||
await expect(page.locator('text=John Doe')).toBeVisible()
|
await expect(page.locator('text=John Doe')).toBeVisible()
|
||||||
await page.click('text=Group #1', { force: true })
|
await page.click('text=Group #1', { force: true })
|
||||||
await expect(page.locator('input[value="Group #1"]')).toBeVisible()
|
await expect(page.getByText('Group #1')).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.describe('Guest on public typebot', () => {
|
||||||
|
test('should have shared typebots displayed', async ({ page }) => {
|
||||||
|
const typebotId = createId()
|
||||||
|
const guestWorkspaceId = createId()
|
||||||
|
await prisma.workspace.create({
|
||||||
|
data: {
|
||||||
|
id: guestWorkspaceId,
|
||||||
|
name: 'Guest Workspace #4',
|
||||||
|
plan: Plan.FREE,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await createTypebots([
|
||||||
|
{
|
||||||
|
id: typebotId,
|
||||||
|
name: 'Guest typebot',
|
||||||
|
workspaceId: guestWorkspaceId,
|
||||||
|
...parseDefaultGroupWithBlock({
|
||||||
|
type: InputBlockType.TEXT,
|
||||||
|
}),
|
||||||
|
settings: {
|
||||||
|
publicShare: { isEnabled: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
|
await expect(page.getByText('Guest typebot')).toBeVisible()
|
||||||
|
await expect(page.getByText('Duplicate')).toBeVisible()
|
||||||
|
await expect(page.getByText('Group #1')).toBeVisible()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export const CollaborationList = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={1} pt="4" pb="2">
|
<Stack spacing={1} pt="4">
|
||||||
<HStack as="form" onSubmit={handleInvitationSubmit} px="4" pb="2">
|
<HStack as="form" onSubmit={handleInvitationSubmit} px="4" pb="2">
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverTrigger,
|
|
||||||
PopoverContent,
|
|
||||||
IconButton,
|
|
||||||
Tooltip,
|
|
||||||
} from '@chakra-ui/react'
|
|
||||||
import { UsersIcon } from '@/components/icons'
|
|
||||||
import React from 'react'
|
|
||||||
import { CollaborationList } from './CollaborationList'
|
|
||||||
|
|
||||||
export const CollaborationMenuButton = ({
|
|
||||||
isLoading,
|
|
||||||
}: {
|
|
||||||
isLoading: boolean
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Popover isLazy placement="bottom-end">
|
|
||||||
<PopoverTrigger>
|
|
||||||
<span>
|
|
||||||
<Tooltip label="Invite users to collaborate">
|
|
||||||
<IconButton
|
|
||||||
isLoading={isLoading}
|
|
||||||
icon={<UsersIcon />}
|
|
||||||
aria-label="Show collaboration menu"
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
|
||||||
shadow="lg"
|
|
||||||
width="430px"
|
|
||||||
rootProps={{ style: { transform: 'scale(0)' } }}
|
|
||||||
>
|
|
||||||
<CollaborationList />
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -26,7 +26,7 @@ export const createCredentials = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/credentials',
|
path: '/v1/credentials',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Create credentials',
|
summary: 'Create credentials',
|
||||||
tags: ['Credentials'],
|
tags: ['Credentials'],
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const deleteCredentials = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
path: '/credentials/:credentialsId',
|
path: '/v1/credentials/:credentialsId',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Delete credentials',
|
summary: 'Delete credentials',
|
||||||
tags: ['Credentials'],
|
tags: ['Credentials'],
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const listCredentials = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/credentials',
|
path: '/v1/credentials',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'List workspace credentials',
|
summary: 'List workspace credentials',
|
||||||
tags: ['Credentials'],
|
tags: ['Credentials'],
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const createCustomDomain = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/custom-domains',
|
path: '/v1/custom-domains',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Create custom domain',
|
summary: 'Create custom domain',
|
||||||
tags: ['Custom domains'],
|
tags: ['Custom domains'],
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const deleteCustomDomain = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
path: '/custom-domains/{name}',
|
path: '/v1/custom-domains/{name}',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Delete custom domain',
|
summary: 'Delete custom domain',
|
||||||
tags: ['Custom domains'],
|
tags: ['Custom domains'],
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const listCustomDomains = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/custom-domains',
|
path: '/v1/custom-domains',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'List custom domains',
|
summary: 'List custom domains',
|
||||||
tags: ['Custom domains'],
|
tags: ['Custom domains'],
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const verifyCustomDomain = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/custom-domains/{name}/verify',
|
path: '/v1/custom-domains/{name}/verify',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Verify domain config',
|
summary: 'Verify domain config',
|
||||||
tags: ['Custom domains'],
|
tags: ['Custom domains'],
|
||||||
|
|||||||
@@ -16,10 +16,17 @@ import { GraphDndProvider } from '@/features/graph/providers/GraphDndProvider'
|
|||||||
import { GraphProvider } from '@/features/graph/providers/GraphProvider'
|
import { GraphProvider } from '@/features/graph/providers/GraphProvider'
|
||||||
import { GroupsCoordinatesProvider } from '@/features/graph/providers/GroupsCoordinateProvider'
|
import { GroupsCoordinatesProvider } from '@/features/graph/providers/GroupsCoordinateProvider'
|
||||||
import { EventsCoordinatesProvider } from '@/features/graph/providers/EventsCoordinateProvider'
|
import { EventsCoordinatesProvider } from '@/features/graph/providers/EventsCoordinateProvider'
|
||||||
|
import { TypebotNotFoundPage } from './TypebotNotFoundPage'
|
||||||
|
|
||||||
export const EditorPage = () => {
|
export const EditorPage = () => {
|
||||||
const { typebot, isReadOnly } = useTypebot()
|
const { typebot, currentUserMode, is404 } = useTypebot()
|
||||||
|
const backgroundImage = useColorModeValue(
|
||||||
|
'radial-gradient(#c6d0e1 1px, transparent 0)',
|
||||||
|
'radial-gradient(#2f2f39 1px, transparent 0)'
|
||||||
|
)
|
||||||
|
const bgColor = useColorModeValue('#f4f5f8', 'gray.850')
|
||||||
|
|
||||||
|
if (is404) return <TypebotNotFoundPage />
|
||||||
return (
|
return (
|
||||||
<EditorProvider>
|
<EditorProvider>
|
||||||
<Seo title={typebot?.name ? `${typebot.name} | Editor` : 'Editor'} />
|
<Seo title={typebot?.name ? `${typebot.name} | Editor` : 'Editor'} />
|
||||||
@@ -30,18 +37,19 @@ export const EditorPage = () => {
|
|||||||
flex="1"
|
flex="1"
|
||||||
pos="relative"
|
pos="relative"
|
||||||
h="full"
|
h="full"
|
||||||
bgColor={useColorModeValue('#f4f5f8', 'gray.850')}
|
bgColor={bgColor}
|
||||||
backgroundImage={useColorModeValue(
|
backgroundImage={backgroundImage}
|
||||||
'radial-gradient(#c6d0e1 1px, transparent 0)',
|
|
||||||
'radial-gradient(#2f2f39 1px, transparent 0)'
|
|
||||||
)}
|
|
||||||
backgroundSize="40px 40px"
|
backgroundSize="40px 40px"
|
||||||
backgroundPosition="-19px -19px"
|
backgroundPosition="-19px -19px"
|
||||||
>
|
>
|
||||||
{typebot ? (
|
{typebot ? (
|
||||||
<GraphDndProvider>
|
<GraphDndProvider>
|
||||||
{!isReadOnly && <BlocksSideBar />}
|
{currentUserMode === 'write' && <BlocksSideBar />}
|
||||||
<GraphProvider isReadOnly={isReadOnly}>
|
<GraphProvider
|
||||||
|
isReadOnly={
|
||||||
|
currentUserMode === 'read' || currentUserMode === 'guest'
|
||||||
|
}
|
||||||
|
>
|
||||||
<GroupsCoordinatesProvider groups={typebot.groups}>
|
<GroupsCoordinatesProvider groups={typebot.groups}>
|
||||||
<EventsCoordinatesProvider events={typebot.events}>
|
<EventsCoordinatesProvider events={typebot.events}>
|
||||||
<Graph flex="1" typebot={typebot} key={typebot.id} />
|
<Graph flex="1" typebot={typebot} key={typebot.id} />
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
BuoyIcon,
|
BuoyIcon,
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
|
PlayIcon,
|
||||||
RedoIcon,
|
RedoIcon,
|
||||||
UndoIcon,
|
UndoIcon,
|
||||||
} from '@/components/icons'
|
} from '@/components/icons'
|
||||||
@@ -23,7 +24,7 @@ import Link from 'next/link'
|
|||||||
import { EditableEmojiOrImageIcon } from '@/components/EditableEmojiOrImageIcon'
|
import { EditableEmojiOrImageIcon } from '@/components/EditableEmojiOrImageIcon'
|
||||||
import { useUndoShortcut } from '@/hooks/useUndoShortcut'
|
import { useUndoShortcut } from '@/hooks/useUndoShortcut'
|
||||||
import { useDebouncedCallback } from 'use-debounce'
|
import { useDebouncedCallback } from 'use-debounce'
|
||||||
import { CollaborationMenuButton } from '@/features/collaboration/components/CollaborationMenuButton'
|
import { ShareTypebotButton } from '@/features/share/components/ShareTypebotButton'
|
||||||
import { PublishButton } from '@/features/publish/components/PublishButton'
|
import { PublishButton } from '@/features/publish/components/PublishButton'
|
||||||
import { headerHeight } from '../constants'
|
import { headerHeight } from '../constants'
|
||||||
import { RightPanel, useEditor } from '../providers/EditorProvider'
|
import { RightPanel, useEditor } from '../providers/EditorProvider'
|
||||||
@@ -31,6 +32,7 @@ import { useTypebot } from '../providers/TypebotProvider'
|
|||||||
import { SupportBubble } from '@/components/SupportBubble'
|
import { SupportBubble } from '@/components/SupportBubble'
|
||||||
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
|
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
|
||||||
import { useTranslate } from '@tolgee/react'
|
import { useTranslate } from '@tolgee/react'
|
||||||
|
import { GuestTypebotHeader } from './UnauthenticatedTypebotHeader'
|
||||||
|
|
||||||
export const TypebotHeader = () => {
|
export const TypebotHeader = () => {
|
||||||
const { t } = useTranslate()
|
const { t } = useTranslate()
|
||||||
@@ -45,6 +47,7 @@ export const TypebotHeader = () => {
|
|||||||
canUndo,
|
canUndo,
|
||||||
canRedo,
|
canRedo,
|
||||||
isSavingLoading,
|
isSavingLoading,
|
||||||
|
currentUserMode,
|
||||||
} = useTypebot()
|
} = useTypebot()
|
||||||
const {
|
const {
|
||||||
setRightPanel,
|
setRightPanel,
|
||||||
@@ -58,6 +61,7 @@ export const TypebotHeader = () => {
|
|||||||
setUndoShortcutTooltipOpen(false)
|
setUndoShortcutTooltipOpen(false)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
const { isOpen, onOpen } = useDisclosure()
|
const { isOpen, onOpen } = useDisclosure()
|
||||||
|
const headerBgColor = useColorModeValue('white', 'gray.900')
|
||||||
|
|
||||||
const handleNameSubmit = (name: string) =>
|
const handleNameSubmit = (name: string) =>
|
||||||
updateTypebot({ updates: { name } })
|
updateTypebot({ updates: { name } })
|
||||||
@@ -86,6 +90,7 @@ export const TypebotHeader = () => {
|
|||||||
: window.open('https://docs.typebot.io', '_blank')
|
: window.open('https://docs.typebot.io', '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentUserMode === 'guest') return <GuestTypebotHeader />
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
w="full"
|
w="full"
|
||||||
@@ -95,7 +100,7 @@ export const TypebotHeader = () => {
|
|||||||
h={`${headerHeight}px`}
|
h={`${headerHeight}px`}
|
||||||
zIndex={100}
|
zIndex={100}
|
||||||
pos="relative"
|
pos="relative"
|
||||||
bgColor={useColorModeValue('white', 'gray.900')}
|
bgColor={headerBgColor}
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
{isOpen && <SupportBubble autoShowDelay={0} />}
|
{isOpen && <SupportBubble autoShowDelay={0} />}
|
||||||
@@ -203,6 +208,7 @@ export const TypebotHeader = () => {
|
|||||||
)
|
)
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
|
{currentUserMode === 'write' && (
|
||||||
<HStack>
|
<HStack>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={isUndoShortcutTooltipOpen ? 'Changes reverted!' : 'Undo'}
|
label={isUndoShortcutTooltipOpen ? 'Changes reverted!' : 'Undo'}
|
||||||
@@ -230,6 +236,7 @@ export const TypebotHeader = () => {
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
)}
|
||||||
<Button leftIcon={<BuoyIcon />} onClick={handleHelpClick} size="sm">
|
<Button leftIcon={<BuoyIcon />} onClick={handleHelpClick} size="sm">
|
||||||
{t('editor.headers.helpButton.label')}
|
{t('editor.headers.helpButton.label')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -246,19 +253,20 @@ export const TypebotHeader = () => {
|
|||||||
|
|
||||||
<HStack right="40px" pos="absolute" display={['none', 'flex']}>
|
<HStack right="40px" pos="absolute" display={['none', 'flex']}>
|
||||||
<Flex pos="relative">
|
<Flex pos="relative">
|
||||||
<CollaborationMenuButton isLoading={isNotDefined(typebot)} />
|
<ShareTypebotButton isLoading={isNotDefined(typebot)} />
|
||||||
</Flex>
|
</Flex>
|
||||||
{router.pathname.includes('/edit') && isNotDefined(rightPanel) && (
|
{router.pathname.includes('/edit') && isNotDefined(rightPanel) && (
|
||||||
<Button
|
<Button
|
||||||
colorScheme="gray"
|
colorScheme="gray"
|
||||||
onClick={handlePreviewClick}
|
onClick={handlePreviewClick}
|
||||||
isLoading={isNotDefined(typebot)}
|
isLoading={isNotDefined(typebot)}
|
||||||
|
leftIcon={<PlayIcon />}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
{t('editor.headers.previewButton.label')}
|
{t('editor.headers.previewButton.label')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<PublishButton size="sm" />
|
{currentUserMode === 'write' && <PublishButton size="sm" />}
|
||||||
</HStack>
|
</HStack>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { ChevronLeftIcon } from '@/components/icons'
|
||||||
|
import { useUser } from '@/features/account/hooks/useUser'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
Link,
|
||||||
|
VStack,
|
||||||
|
Text,
|
||||||
|
Spinner,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
export const TypebotNotFoundPage = () => {
|
||||||
|
const { replace, asPath } = useRouter()
|
||||||
|
const { user, isLoading } = useUser()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user || isLoading) return
|
||||||
|
replace({
|
||||||
|
pathname: '/signin',
|
||||||
|
query: {
|
||||||
|
redirectPath: asPath,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, [asPath, isLoading, replace, user])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex justify="center" align="center" w="full" h="100vh">
|
||||||
|
{user ? (
|
||||||
|
<VStack spacing={6}>
|
||||||
|
<VStack>
|
||||||
|
<Heading>404</Heading>
|
||||||
|
<Text fontSize="xl">Typebot not found.</Text>
|
||||||
|
</VStack>
|
||||||
|
<Button
|
||||||
|
as={Link}
|
||||||
|
href="/typebots"
|
||||||
|
colorScheme="blue"
|
||||||
|
leftIcon={<ChevronLeftIcon />}
|
||||||
|
>
|
||||||
|
Dashboard
|
||||||
|
</Button>
|
||||||
|
</VStack>
|
||||||
|
) : (
|
||||||
|
<Spinner />
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import {
|
||||||
|
Flex,
|
||||||
|
HStack,
|
||||||
|
Button,
|
||||||
|
useColorModeValue,
|
||||||
|
Divider,
|
||||||
|
Text,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { CopyIcon, PlayIcon } from '@/components/icons'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import React from 'react'
|
||||||
|
import { isNotDefined } from '@typebot.io/lib'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { headerHeight } from '../constants'
|
||||||
|
import { RightPanel, useEditor } from '../providers/EditorProvider'
|
||||||
|
import { useTypebot } from '../providers/TypebotProvider'
|
||||||
|
import { useTranslate } from '@tolgee/react'
|
||||||
|
import { TypebotLogo } from '@/components/TypebotLogo'
|
||||||
|
import { EmojiOrImageIcon } from '@/components/EmojiOrImageIcon'
|
||||||
|
import { useUser } from '@/features/account/hooks/useUser'
|
||||||
|
|
||||||
|
export const GuestTypebotHeader = () => {
|
||||||
|
const { t } = useTranslate()
|
||||||
|
const router = useRouter()
|
||||||
|
const { user } = useUser()
|
||||||
|
const { typebot, save } = useTypebot()
|
||||||
|
const {
|
||||||
|
setRightPanel,
|
||||||
|
rightPanel,
|
||||||
|
setStartPreviewAtGroup,
|
||||||
|
setStartPreviewAtEvent,
|
||||||
|
} = useEditor()
|
||||||
|
|
||||||
|
const handlePreviewClick = async () => {
|
||||||
|
setStartPreviewAtGroup(undefined)
|
||||||
|
setStartPreviewAtEvent(undefined)
|
||||||
|
save().then()
|
||||||
|
setRightPanel(RightPanel.PREVIEW)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
w="full"
|
||||||
|
borderBottomWidth="1px"
|
||||||
|
justify="center"
|
||||||
|
align="center"
|
||||||
|
h={`${headerHeight}px`}
|
||||||
|
zIndex={100}
|
||||||
|
pos="relative"
|
||||||
|
bgColor={useColorModeValue('white', 'gray.900')}
|
||||||
|
flexShrink={0}
|
||||||
|
>
|
||||||
|
<HStack
|
||||||
|
display={['none', 'flex']}
|
||||||
|
pos={{ base: 'absolute', xl: 'static' }}
|
||||||
|
right={{ base: 280, xl: 0 }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
as={Link}
|
||||||
|
href={`/typebots/${typebot?.id}/edit`}
|
||||||
|
colorScheme={router.pathname.includes('/edit') ? 'blue' : 'gray'}
|
||||||
|
variant={router.pathname.includes('/edit') ? 'outline' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{t('editor.headers.flowButton.label')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
as={Link}
|
||||||
|
href={`/typebots/${typebot?.id}/theme`}
|
||||||
|
colorScheme={router.pathname.endsWith('theme') ? 'blue' : 'gray'}
|
||||||
|
variant={router.pathname.endsWith('theme') ? 'outline' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{t('editor.headers.themeButton.label')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
as={Link}
|
||||||
|
href={`/typebots/${typebot?.id}/settings`}
|
||||||
|
colorScheme={router.pathname.endsWith('settings') ? 'blue' : 'gray'}
|
||||||
|
variant={router.pathname.endsWith('settings') ? 'outline' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{t('editor.headers.settingsButton.label')}
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
<HStack
|
||||||
|
pos="absolute"
|
||||||
|
left="1rem"
|
||||||
|
justify="center"
|
||||||
|
align="center"
|
||||||
|
spacing="6"
|
||||||
|
>
|
||||||
|
<HStack alignItems="center" spacing={3}>
|
||||||
|
{typebot && (
|
||||||
|
<EmojiOrImageIcon icon={typebot.icon} emojiFontSize="2xl" />
|
||||||
|
)}
|
||||||
|
<Text
|
||||||
|
noOfLines={2}
|
||||||
|
maxW="150px"
|
||||||
|
overflow="hidden"
|
||||||
|
fontSize="14px"
|
||||||
|
minW="30px"
|
||||||
|
minH="20px"
|
||||||
|
>
|
||||||
|
{typebot?.name}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<HStack
|
||||||
|
right="1rem"
|
||||||
|
pos="absolute"
|
||||||
|
display={['none', 'flex']}
|
||||||
|
spacing={4}
|
||||||
|
>
|
||||||
|
<HStack>
|
||||||
|
{typebot?.id && (
|
||||||
|
<Button
|
||||||
|
as={Link}
|
||||||
|
href={
|
||||||
|
!user
|
||||||
|
? {
|
||||||
|
pathname: `/register`,
|
||||||
|
query: {
|
||||||
|
redirectPath: `/typebots/${typebot.id}/duplicate`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: `/typebots/${typebot.id}/duplicate`
|
||||||
|
}
|
||||||
|
leftIcon={<CopyIcon />}
|
||||||
|
isLoading={isNotDefined(typebot)}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Duplicate
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{router.pathname.includes('/edit') && isNotDefined(rightPanel) && (
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
onClick={handlePreviewClick}
|
||||||
|
isLoading={isNotDefined(typebot)}
|
||||||
|
leftIcon={<PlayIcon />}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Play bot
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{!user && (
|
||||||
|
<>
|
||||||
|
<Divider orientation="vertical" h="25px" borderColor="gray.400" />
|
||||||
|
<Button
|
||||||
|
as={Link}
|
||||||
|
href={`/register`}
|
||||||
|
leftIcon={<TypebotLogo width="20px" />}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Try Typebot
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -207,7 +207,7 @@ test('Preview from group should work', async ({ page }) => {
|
|||||||
page.locator('typebot-standard').locator('text="Hello this is group 2"')
|
page.locator('typebot-standard').locator('text="Hello this is group 2"')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
await page.click('[aria-label="Close"]')
|
await page.click('[aria-label="Close"]')
|
||||||
await page.click('text="Preview"')
|
await page.click('text="Test"')
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('typebot-standard').locator('text="Hello this is group 1"')
|
page.locator('typebot-standard').locator('text="Hello this is group 1"')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
|
|||||||
@@ -23,8 +23,11 @@ const initialState = {
|
|||||||
future: [],
|
future: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Params = { isReadOnly?: boolean }
|
||||||
|
|
||||||
export const useUndo = <T extends { updatedAt: Date }>(
|
export const useUndo = <T extends { updatedAt: Date }>(
|
||||||
initialPresent?: T
|
initialPresent?: T,
|
||||||
|
params?: Params
|
||||||
): [T | undefined, Actions<T>] => {
|
): [T | undefined, Actions<T>] => {
|
||||||
const [history, setHistory] = useState<History<T>>(initialState)
|
const [history, setHistory] = useState<History<T>>(initialState)
|
||||||
const presentRef = useRef<T | null>(initialPresent ?? null)
|
const presentRef = useRef<T | null>(initialPresent ?? null)
|
||||||
@@ -33,6 +36,7 @@ export const useUndo = <T extends { updatedAt: Date }>(
|
|||||||
const canRedo = history.future.length !== 0
|
const canRedo = history.future.length !== 0
|
||||||
|
|
||||||
const undo = useCallback(() => {
|
const undo = useCallback(() => {
|
||||||
|
if (params?.isReadOnly) return
|
||||||
const { past, present, future } = history
|
const { past, present, future } = history
|
||||||
if (past.length === 0 || !present) return
|
if (past.length === 0 || !present) return
|
||||||
|
|
||||||
@@ -47,9 +51,10 @@ export const useUndo = <T extends { updatedAt: Date }>(
|
|||||||
future: [present, ...future],
|
future: [present, ...future],
|
||||||
})
|
})
|
||||||
presentRef.current = newPresent
|
presentRef.current = newPresent
|
||||||
}, [history])
|
}, [history, params?.isReadOnly])
|
||||||
|
|
||||||
const redo = useCallback(() => {
|
const redo = useCallback(() => {
|
||||||
|
if (params?.isReadOnly) return
|
||||||
const { past, present, future } = history
|
const { past, present, future } = history
|
||||||
if (future.length === 0) return
|
if (future.length === 0) return
|
||||||
const next = future[0]
|
const next = future[0]
|
||||||
@@ -61,11 +66,12 @@ export const useUndo = <T extends { updatedAt: Date }>(
|
|||||||
future: newFuture,
|
future: newFuture,
|
||||||
})
|
})
|
||||||
presentRef.current = next
|
presentRef.current = next
|
||||||
}, [history])
|
}, [history, params?.isReadOnly])
|
||||||
|
|
||||||
const set = useCallback(
|
const set = useCallback(
|
||||||
(newPresentArg: T | ((current: T) => T) | undefined) => {
|
(newPresentArg: T | ((current: T) => T) | undefined) => {
|
||||||
const { past, present } = history
|
const { past, present } = history
|
||||||
|
if (isDefined(present) && params?.isReadOnly) return
|
||||||
const newPresent =
|
const newPresent =
|
||||||
typeof newPresentArg === 'function'
|
typeof newPresentArg === 'function'
|
||||||
? newPresentArg(presentRef.current as T)
|
? newPresentArg(presentRef.current as T)
|
||||||
@@ -92,16 +98,17 @@ export const useUndo = <T extends { updatedAt: Date }>(
|
|||||||
})
|
})
|
||||||
presentRef.current = newPresent
|
presentRef.current = newPresent
|
||||||
},
|
},
|
||||||
[history]
|
[history, params?.isReadOnly]
|
||||||
)
|
)
|
||||||
|
|
||||||
const flush = useCallback(() => {
|
const flush = useCallback(() => {
|
||||||
|
if (params?.isReadOnly) return
|
||||||
setHistory({
|
setHistory({
|
||||||
present: presentRef.current ?? undefined,
|
present: presentRef.current ?? undefined,
|
||||||
past: [],
|
past: [],
|
||||||
future: [],
|
future: [],
|
||||||
})
|
})
|
||||||
}, [])
|
}, [params?.isReadOnly])
|
||||||
|
|
||||||
return [history.present, { set, undo, redo, flush, canUndo, canRedo }]
|
return [history.present, { set, undo, redo, flush, canUndo, canRedo }]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { isDefined, omit } from '@typebot.io/lib'
|
import { isDefined, omit } from '@typebot.io/lib'
|
||||||
import { edgesAction, EdgesActions } from './typebotActions/edges'
|
import { edgesAction, EdgesActions } from './typebotActions/edges'
|
||||||
@@ -52,7 +53,8 @@ const typebotContext = createContext<
|
|||||||
typebot?: TypebotV6
|
typebot?: TypebotV6
|
||||||
publishedTypebot?: PublicTypebotV6
|
publishedTypebot?: PublicTypebotV6
|
||||||
publishedTypebotVersion?: PublicTypebot['version']
|
publishedTypebotVersion?: PublicTypebot['version']
|
||||||
isReadOnly?: boolean
|
currentUserMode: 'guest' | 'read' | 'write'
|
||||||
|
is404: boolean
|
||||||
isPublished: boolean
|
isPublished: boolean
|
||||||
isSavingLoading: boolean
|
isSavingLoading: boolean
|
||||||
save: () => Promise<TypebotV6 | undefined>
|
save: () => Promise<TypebotV6 | undefined>
|
||||||
@@ -84,6 +86,7 @@ export const TypebotProvider = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { push } = useRouter()
|
const { push } = useRouter()
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
|
const [is404, setIs404] = useState(false)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: typebotData,
|
data: typebotData,
|
||||||
@@ -96,13 +99,10 @@ export const TypebotProvider = ({
|
|||||||
retry: 0,
|
retry: 0,
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
if (error.data?.httpStatus === 404) {
|
if (error.data?.httpStatus === 404) {
|
||||||
showToast({
|
setIs404(true)
|
||||||
status: 'info',
|
|
||||||
description: "Couldn't find typebot. Redirecting...",
|
|
||||||
})
|
|
||||||
push('/typebots')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
setIs404(false)
|
||||||
showToast({
|
showToast({
|
||||||
title: 'Could not fetch typebot',
|
title: 'Could not fetch typebot',
|
||||||
description: error.message,
|
description: error.message,
|
||||||
@@ -112,6 +112,9 @@ export const TypebotProvider = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
setIs404(false)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -119,7 +122,10 @@ export const TypebotProvider = ({
|
|||||||
trpc.typebot.getPublishedTypebot.useQuery(
|
trpc.typebot.getPublishedTypebot.useQuery(
|
||||||
{ typebotId: typebotId as string, migrateToLatestVersion: true },
|
{ typebotId: typebotId as string, migrateToLatestVersion: true },
|
||||||
{
|
{
|
||||||
enabled: isDefined(typebotId),
|
enabled:
|
||||||
|
isDefined(typebotId) &&
|
||||||
|
(typebotData?.currentUserMode === 'read' ||
|
||||||
|
typebotData?.currentUserMode === 'write'),
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
showToast({
|
showToast({
|
||||||
title: 'Could not fetch published typebot',
|
title: 'Could not fetch published typebot',
|
||||||
@@ -153,11 +159,16 @@ export const TypebotProvider = ({
|
|||||||
const typebot = typebotData?.typebot as TypebotV6
|
const typebot = typebotData?.typebot as TypebotV6
|
||||||
const publishedTypebot = (publishedTypebotData?.publishedTypebot ??
|
const publishedTypebot = (publishedTypebotData?.publishedTypebot ??
|
||||||
undefined) as PublicTypebotV6 | undefined
|
undefined) as PublicTypebotV6 | undefined
|
||||||
|
const isReadOnly = ['read', 'guest'].includes(
|
||||||
|
typebotData?.currentUserMode ?? 'guest'
|
||||||
|
)
|
||||||
|
|
||||||
const [
|
const [
|
||||||
localTypebot,
|
localTypebot,
|
||||||
{ redo, undo, flush, canRedo, canUndo, set: setLocalTypebot },
|
{ redo, undo, flush, canRedo, canUndo, set: setLocalTypebot },
|
||||||
] = useUndo<TypebotV6>(undefined)
|
] = useUndo<TypebotV6>(undefined, {
|
||||||
|
isReadOnly,
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!typebot && isDefined(localTypebot)) setLocalTypebot(undefined)
|
if (!typebot && isDefined(localTypebot)) setLocalTypebot(undefined)
|
||||||
@@ -182,7 +193,7 @@ export const TypebotProvider = ({
|
|||||||
|
|
||||||
const saveTypebot = useCallback(
|
const saveTypebot = useCallback(
|
||||||
async (updates?: Partial<TypebotV6>) => {
|
async (updates?: Partial<TypebotV6>) => {
|
||||||
if (!localTypebot || !typebot || typebotData?.isReadOnly) return
|
if (!localTypebot || !typebot || 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
|
||||||
@@ -194,13 +205,7 @@ export const TypebotProvider = ({
|
|||||||
setLocalTypebot({ ...newTypebot })
|
setLocalTypebot({ ...newTypebot })
|
||||||
return newTypebot
|
return newTypebot
|
||||||
},
|
},
|
||||||
[
|
[isReadOnly, localTypebot, setLocalTypebot, typebot, updateTypebot]
|
||||||
localTypebot,
|
|
||||||
setLocalTypebot,
|
|
||||||
typebot,
|
|
||||||
typebotData?.isReadOnly,
|
|
||||||
updateTypebot,
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
useAutoSave(
|
useAutoSave(
|
||||||
@@ -232,7 +237,7 @@ export const TypebotProvider = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!localTypebot || !typebot || typebotData?.isReadOnly) return
|
if (!localTypebot || !typebot || isReadOnly) return
|
||||||
if (!areTypebotsEqual(localTypebot, typebot)) {
|
if (!areTypebotsEqual(localTypebot, typebot)) {
|
||||||
window.addEventListener('beforeunload', preventUserFromRefreshing)
|
window.addEventListener('beforeunload', preventUserFromRefreshing)
|
||||||
}
|
}
|
||||||
@@ -240,7 +245,7 @@ export const TypebotProvider = ({
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('beforeunload', preventUserFromRefreshing)
|
window.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||||
}
|
}
|
||||||
}, [localTypebot, typebot, typebotData?.isReadOnly])
|
}, [localTypebot, typebot, isReadOnly])
|
||||||
|
|
||||||
const updateLocalTypebot = async ({
|
const updateLocalTypebot = async ({
|
||||||
updates,
|
updates,
|
||||||
@@ -249,7 +254,7 @@ export const TypebotProvider = ({
|
|||||||
updates: UpdateTypebotPayload
|
updates: UpdateTypebotPayload
|
||||||
save?: boolean
|
save?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
if (!localTypebot) return
|
if (!localTypebot || isReadOnly) return
|
||||||
const newTypebot = { ...localTypebot, ...updates }
|
const newTypebot = { ...localTypebot, ...updates }
|
||||||
setLocalTypebot(newTypebot)
|
setLocalTypebot(newTypebot)
|
||||||
if (save) await saveTypebot(newTypebot)
|
if (save) await saveTypebot(newTypebot)
|
||||||
@@ -269,8 +274,9 @@ export const TypebotProvider = ({
|
|||||||
typebot: localTypebot,
|
typebot: localTypebot,
|
||||||
publishedTypebot,
|
publishedTypebot,
|
||||||
publishedTypebotVersion: publishedTypebotData?.version,
|
publishedTypebotVersion: publishedTypebotData?.version,
|
||||||
isReadOnly: typebotData?.isReadOnly,
|
currentUserMode: typebotData?.currentUserMode ?? 'guest',
|
||||||
isSavingLoading: isSaving,
|
isSavingLoading: isSaving,
|
||||||
|
is404,
|
||||||
save: saveTypebot,
|
save: saveTypebot,
|
||||||
undo,
|
undo,
|
||||||
redo,
|
redo,
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ export const BlockNode = ({
|
|||||||
const handleClick = (e: React.MouseEvent) => {
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
setFocusedGroupId(groupId)
|
setFocusedGroupId(groupId)
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (isTextBubbleBlock(block)) setIsEditing(true)
|
if (isTextBubbleBlock(block) && !isReadOnly) setIsEditing(true)
|
||||||
setOpenedBlockId(block.id)
|
setOpenedBlockId(block.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,11 +112,7 @@ export const BlockNodesList = ({ blocks, groupIndex, groupRef }: Props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack spacing={1} transition="none">
|
||||||
spacing={1}
|
|
||||||
transition="none"
|
|
||||||
pointerEvents={isReadOnly ? 'none' : 'auto'}
|
|
||||||
>
|
|
||||||
<PlaceholderNode
|
<PlaceholderNode
|
||||||
isVisible={showSortPlaceholders}
|
isVisible={showSortPlaceholders}
|
||||||
isExpanded={expandedPlaceholderIndex === 0}
|
isExpanded={expandedPlaceholderIndex === 0}
|
||||||
|
|||||||
@@ -227,7 +227,6 @@ const NonMemoizedDraggableGroupNode = ({
|
|||||||
onChange={setGroupTitle}
|
onChange={setGroupTitle}
|
||||||
onSubmit={handleTitleSubmit}
|
onSubmit={handleTitleSubmit}
|
||||||
fontWeight="semibold"
|
fontWeight="semibold"
|
||||||
pointerEvents={isReadOnly ? 'none' : 'auto'}
|
|
||||||
pr="8"
|
pr="8"
|
||||||
>
|
>
|
||||||
<EditablePreview
|
<EditablePreview
|
||||||
|
|||||||
@@ -27,11 +27,12 @@ import { parseDefaultPublicId } from '../helpers/parseDefaultPublicId'
|
|||||||
import { useTranslate } from '@tolgee/react'
|
import { useTranslate } from '@tolgee/react'
|
||||||
import { env } from '@typebot.io/env'
|
import { env } from '@typebot.io/env'
|
||||||
import DomainStatusIcon from '@/features/customDomains/components/DomainStatusIcon'
|
import DomainStatusIcon from '@/features/customDomains/components/DomainStatusIcon'
|
||||||
|
import { TypebotNotFoundPage } from '@/features/editor/components/TypebotNotFoundPage'
|
||||||
|
|
||||||
export const SharePage = () => {
|
export const SharePage = () => {
|
||||||
const { t } = useTranslate()
|
const { t } = useTranslate()
|
||||||
const { workspace } = useWorkspace()
|
const { workspace } = useWorkspace()
|
||||||
const { typebot, updateTypebot, publishedTypebot } = useTypebot()
|
const { typebot, updateTypebot, publishedTypebot, is404 } = useTypebot()
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
|
|
||||||
const handlePublicIdChange = async (publicId: string) => {
|
const handlePublicIdChange = async (publicId: string) => {
|
||||||
@@ -87,6 +88,7 @@ export const SharePage = () => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (is404) return <TypebotNotFoundPage />
|
||||||
return (
|
return (
|
||||||
<Flex flexDir="column" pb="40">
|
<Flex flexDir="column" pb="40">
|
||||||
<Seo title={typebot?.name ? `${typebot.name} | Share` : 'Share'} />
|
<Seo title={typebot?.name ? `${typebot.name} | Share` : 'Share'} />
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const deleteResults = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
path: '/typebots/{typebotId}/results',
|
path: '/v1/typebots/{typebotId}/results',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Delete results',
|
summary: 'Delete results',
|
||||||
tags: ['Results'],
|
tags: ['Results'],
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const getResult = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/typebots/{typebotId}/results/{resultId}',
|
path: '/v1/typebots/{typebotId}/results/{resultId}',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Get result by id',
|
summary: 'Get result by id',
|
||||||
tags: ['Results'],
|
tags: ['Results'],
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const getResultLogs = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/typebots/{typebotId}/results/{resultId}/logs',
|
path: '/v1/typebots/{typebotId}/results/{resultId}/logs',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'List result logs',
|
summary: 'List result logs',
|
||||||
tags: ['Results'],
|
tags: ['Results'],
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const getResults = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/typebots/{typebotId}/results',
|
path: '/v1/typebots/{typebotId}/results',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'List results ordered by descending creation date',
|
summary: 'List results ordered by descending creation date',
|
||||||
tags: ['Results'],
|
tags: ['Results'],
|
||||||
|
|||||||
@@ -18,15 +18,20 @@ import { useMemo } from 'react'
|
|||||||
import { useStats } from '../hooks/useStats'
|
import { useStats } from '../hooks/useStats'
|
||||||
import { ResultsProvider } from '../ResultsProvider'
|
import { ResultsProvider } from '../ResultsProvider'
|
||||||
import { ResultsTableContainer } from './ResultsTableContainer'
|
import { ResultsTableContainer } from './ResultsTableContainer'
|
||||||
|
import { TypebotNotFoundPage } from '@/features/editor/components/TypebotNotFoundPage'
|
||||||
|
|
||||||
export const ResultsPage = () => {
|
export const ResultsPage = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { workspace } = useWorkspace()
|
const { workspace } = useWorkspace()
|
||||||
const { typebot, publishedTypebot } = useTypebot()
|
const { typebot, publishedTypebot, is404 } = useTypebot()
|
||||||
const isAnalytics = useMemo(
|
const isAnalytics = useMemo(
|
||||||
() => router.pathname.endsWith('analytics'),
|
() => router.pathname.endsWith('analytics'),
|
||||||
[router.pathname]
|
[router.pathname]
|
||||||
)
|
)
|
||||||
|
const bgColor = useColorModeValue(
|
||||||
|
router.pathname.endsWith('analytics') ? '#f4f5f8' : 'white',
|
||||||
|
router.pathname.endsWith('analytics') ? 'gray.850' : 'gray.900'
|
||||||
|
)
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
|
|
||||||
const { stats, mutate } = useStats({
|
const { stats, mutate } = useStats({
|
||||||
@@ -41,6 +46,7 @@ export const ResultsPage = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (is404) return <TypebotNotFoundPage />
|
||||||
return (
|
return (
|
||||||
<Flex overflow="hidden" h="100vh" flexDir="column">
|
<Flex overflow="hidden" h="100vh" flexDir="column">
|
||||||
<Seo
|
<Seo
|
||||||
@@ -55,14 +61,7 @@ export const ResultsPage = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TypebotHeader />
|
<TypebotHeader />
|
||||||
<Flex
|
<Flex h="full" w="full" bgColor={bgColor}>
|
||||||
h="full"
|
|
||||||
w="full"
|
|
||||||
bgColor={useColorModeValue(
|
|
||||||
router.pathname.endsWith('analytics') ? '#f4f5f8' : 'white',
|
|
||||||
router.pathname.endsWith('analytics') ? 'gray.850' : 'gray.900'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Flex
|
<Flex
|
||||||
pos="absolute"
|
pos="absolute"
|
||||||
zIndex={2}
|
zIndex={2}
|
||||||
|
|||||||
@@ -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, isReadOnly } = useTypebot()
|
const { updateTypebot, currentUserMode } = 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)
|
||||||
@@ -212,7 +212,7 @@ export const ResultsTable = ({
|
|||||||
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">
|
||||||
{isReadOnly ? null : (
|
{currentUserMode === 'write' && (
|
||||||
<SelectionToolbar
|
<SelectionToolbar
|
||||||
selectedResultsId={Object.keys(rowSelection)}
|
selectedResultsId={Object.keys(rowSelection)}
|
||||||
onClearSelection={() => setRowSelection({})}
|
onClearSelection={() => setRowSelection({})}
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import { getViewerUrl } from '@typebot.io/lib/getViewerUrl'
|
|||||||
import { SettingsSideMenu } from './SettingsSideMenu'
|
import { SettingsSideMenu } from './SettingsSideMenu'
|
||||||
import { TypebotHeader } from '@/features/editor/components/TypebotHeader'
|
import { TypebotHeader } from '@/features/editor/components/TypebotHeader'
|
||||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
|
import { TypebotNotFoundPage } from '@/features/editor/components/TypebotNotFoundPage'
|
||||||
|
|
||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const { typebot } = useTypebot()
|
const { typebot, is404 } = useTypebot()
|
||||||
|
|
||||||
|
if (is404) return <TypebotNotFoundPage />
|
||||||
return (
|
return (
|
||||||
<Flex overflow="hidden" h="100vh" flexDir="column">
|
<Flex overflow="hidden" h="100vh" flexDir="column">
|
||||||
<Seo title={typebot?.name ? `${typebot.name} | Settings` : 'Settings'} />
|
<Seo title={typebot?.name ? `${typebot.name} | Settings` : 'Settings'} />
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Stack, Input, InputGroup, InputRightElement } from '@chakra-ui/react'
|
||||||
|
import React, { useMemo } from 'react'
|
||||||
|
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
|
||||||
|
import { CopyButton } from '@/components/CopyButton'
|
||||||
|
import { CollaborationList } from '@/features/collaboration/components/CollaborationList'
|
||||||
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
|
|
||||||
|
export const SharePopoverContent = () => {
|
||||||
|
const { typebot, updateTypebot } = useTypebot()
|
||||||
|
|
||||||
|
const currentUrl = useMemo(() => window.location.href.split('?')[0], [])
|
||||||
|
|
||||||
|
const updateIsPublicShareEnabled = async (isEnabled: boolean) => {
|
||||||
|
await updateTypebot({
|
||||||
|
updates: {
|
||||||
|
settings: {
|
||||||
|
...typebot?.settings,
|
||||||
|
publicShare: {
|
||||||
|
...typebot?.settings.publicShare,
|
||||||
|
isEnabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
save: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<CollaborationList />
|
||||||
|
<Stack p="4" borderTopWidth={1}>
|
||||||
|
<SwitchWithRelatedSettings
|
||||||
|
label={'Make the flow publicly available'}
|
||||||
|
initialValue={typebot?.settings.publicShare?.isEnabled ?? false}
|
||||||
|
onCheckChange={updateIsPublicShareEnabled}
|
||||||
|
>
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<InputGroup size="sm">
|
||||||
|
<Input type={'text'} defaultValue={currentUrl} pr="16" />
|
||||||
|
<InputRightElement width="60px">
|
||||||
|
<CopyButton size="sm" textToCopy={currentUrl} />
|
||||||
|
</InputRightElement>
|
||||||
|
</InputGroup>
|
||||||
|
</Stack>
|
||||||
|
</SwitchWithRelatedSettings>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
Button,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { UsersIcon } from '@/components/icons'
|
||||||
|
import React from 'react'
|
||||||
|
import { SharePopoverContent } from './SharePopoverContent'
|
||||||
|
|
||||||
|
export const ShareTypebotButton = ({ isLoading }: { isLoading: boolean }) => {
|
||||||
|
return (
|
||||||
|
<Popover isLazy placement="bottom-end">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
leftIcon={<UsersIcon />}
|
||||||
|
aria-label="Open share popover"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Share
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
shadow="lg"
|
||||||
|
width="430px"
|
||||||
|
rootProps={{ style: { transform: 'scale(0)' } }}
|
||||||
|
>
|
||||||
|
<SharePopoverContent />
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import got from 'got'
|
|||||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
import { env } from '@typebot.io/env'
|
import { env } from '@typebot.io/env'
|
||||||
|
|
||||||
// Only used for the cloud version of Typebot. It's the way it processes telemetry events and inject it to thrid-party services.
|
|
||||||
export const processTelemetryEvent = authenticatedProcedure
|
export const processTelemetryEvent = authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const deleteThemeTemplate = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
path: '/themeTemplates/{themeTemplateId}',
|
path: '/v1/themeTemplates/{themeTemplateId}',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Delete a theme template',
|
summary: 'Delete a theme template',
|
||||||
tags: ['Theme template'],
|
tags: ['Theme template'],
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const listThemeTemplates = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/themeTemplates',
|
path: '/v1/themeTemplates',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'List theme templates',
|
summary: 'List theme templates',
|
||||||
tags: ['Theme template'],
|
tags: ['Theme template'],
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const saveThemeTemplate = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
path: '/themeTemplates/{themeTemplateId}',
|
path: '/v1/themeTemplates/{themeTemplateId}',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Save theme template',
|
summary: 'Save theme template',
|
||||||
tags: ['Theme template'],
|
tags: ['Theme template'],
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
|||||||
import { Flex } from '@chakra-ui/react'
|
import { Flex } from '@chakra-ui/react'
|
||||||
import { Standard } from '@typebot.io/nextjs'
|
import { Standard } from '@typebot.io/nextjs'
|
||||||
import { ThemeSideMenu } from './ThemeSideMenu'
|
import { ThemeSideMenu } from './ThemeSideMenu'
|
||||||
|
import { TypebotNotFoundPage } from '@/features/editor/components/TypebotNotFoundPage'
|
||||||
|
|
||||||
export const ThemePage = () => {
|
export const ThemePage = () => {
|
||||||
const { typebot } = useTypebot()
|
const { typebot, is404 } = useTypebot()
|
||||||
|
|
||||||
|
if (is404) return <TypebotNotFoundPage />
|
||||||
return (
|
return (
|
||||||
<Flex overflow="hidden" h="100vh" flexDir="column">
|
<Flex overflow="hidden" h="100vh" flexDir="column">
|
||||||
<Seo title={typebot?.name ? `${typebot.name} | Theme` : 'Theme'} />
|
<Seo title={typebot?.name ? `${typebot.name} | Theme` : 'Theme'} />
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
Heading,
|
Heading,
|
||||||
HStack,
|
HStack,
|
||||||
Stack,
|
Stack,
|
||||||
Tag,
|
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { ChatIcon, CodeIcon, DropletIcon, TableIcon } from '@/components/icons'
|
import { ChatIcon, CodeIcon, DropletIcon, TableIcon } from '@/components/icons'
|
||||||
import { ChatTheme, GeneralTheme, ThemeTemplate } from '@typebot.io/schemas'
|
import { ChatTheme, GeneralTheme, ThemeTemplate } from '@typebot.io/schemas'
|
||||||
@@ -21,7 +20,7 @@ import { ThemeTemplates } from './ThemeTemplates'
|
|||||||
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
|
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
|
||||||
|
|
||||||
export const ThemeSideMenu = () => {
|
export const ThemeSideMenu = () => {
|
||||||
const { typebot, updateTypebot } = useTypebot()
|
const { typebot, updateTypebot, currentUserMode } = useTypebot()
|
||||||
|
|
||||||
const updateChatTheme = (chat: ChatTheme) =>
|
const updateChatTheme = (chat: ChatTheme) =>
|
||||||
typebot && updateTypebot({ updates: { theme: { ...typebot.theme, chat } } })
|
typebot && updateTypebot({ updates: { theme: { ...typebot.theme, chat } } })
|
||||||
@@ -71,15 +70,12 @@ export const ThemeSideMenu = () => {
|
|||||||
Customize the theme
|
Customize the theme
|
||||||
</Heading>
|
</Heading>
|
||||||
<Accordion allowMultiple>
|
<Accordion allowMultiple>
|
||||||
|
{currentUserMode === 'write' && (
|
||||||
<AccordionItem>
|
<AccordionItem>
|
||||||
<AccordionButton py={6}>
|
<AccordionButton py={6}>
|
||||||
<HStack flex="1" pl={2}>
|
<HStack flex="1" pl={2}>
|
||||||
<TableIcon />
|
<TableIcon />
|
||||||
<Heading fontSize="lg">
|
<Heading fontSize="lg">Templates</Heading>
|
||||||
<HStack>
|
|
||||||
<span>Templates</span> <Tag colorScheme="orange">New!</Tag>
|
|
||||||
</HStack>
|
|
||||||
</Heading>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
<AccordionIcon />
|
<AccordionIcon />
|
||||||
</AccordionButton>
|
</AccordionButton>
|
||||||
@@ -96,6 +92,7 @@ export const ThemeSideMenu = () => {
|
|||||||
)}
|
)}
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
)}
|
||||||
<AccordionItem>
|
<AccordionItem>
|
||||||
<AccordionButton py={6}>
|
<AccordionButton py={6}>
|
||||||
<HStack flex="1" pl={2}>
|
<HStack flex="1" pl={2}>
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ test.describe.parallel('Theme page', () => {
|
|||||||
})
|
})
|
||||||
await page.goto(`/typebots/${typebotId}/theme`)
|
await page.goto(`/typebots/${typebotId}/theme`)
|
||||||
await expect(page.getByRole('button', { name: 'Go' })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Go' })).toBeVisible()
|
||||||
await page.getByRole('button', { name: 'Templates New!' }).click()
|
await page.getByRole('button', { name: 'Templates' }).click()
|
||||||
await page.getByRole('button', { name: 'Save current theme' }).click()
|
await page.getByRole('button', { name: 'Save current theme' }).click()
|
||||||
await page.getByPlaceholder('My template').fill('My awesome theme')
|
await page.getByPlaceholder('My template').fill('My awesome theme')
|
||||||
await page.getByRole('button', { name: 'Save' }).click()
|
await page.getByRole('button', { name: 'Save' }).click()
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const createTypebot = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/typebots',
|
path: '/v1/typebots',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Create a typebot',
|
summary: 'Create a typebot',
|
||||||
tags: ['Typebot'],
|
tags: ['Typebot'],
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const deleteTypebot = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
path: '/typebots/{typebotId}',
|
path: '/v1/typebots/{typebotId}',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Delete a typebot',
|
summary: 'Delete a typebot',
|
||||||
tags: ['Typebot'],
|
tags: ['Typebot'],
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const getPublishedTypebot = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/typebots/{typebotId}/publishedTypebot',
|
path: '/v1/typebots/{typebotId}/publishedTypebot',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Get published typebot',
|
summary: 'Get published typebot',
|
||||||
tags: ['Typebot'],
|
tags: ['Typebot'],
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import prisma from '@typebot.io/lib/prisma'
|
import prisma from '@typebot.io/lib/prisma'
|
||||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
import { publicProcedure } from '@/helpers/server/trpc'
|
||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
import { typebotSchema } from '@typebot.io/schemas'
|
import { typebotSchema } from '@typebot.io/schemas'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { isReadTypebotForbidden } from '../helpers/isReadTypebotForbidden'
|
import { isReadTypebotForbidden } from '../helpers/isReadTypebotForbidden'
|
||||||
import { migrateTypebot } from '@typebot.io/lib/migrations/migrateTypebot'
|
import { migrateTypebot } from '@typebot.io/lib/migrations/migrateTypebot'
|
||||||
|
import { CollaborationType } from '@typebot.io/prisma'
|
||||||
|
|
||||||
export const getTypebot = authenticatedProcedure
|
export const getTypebot = publicProcedure
|
||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/typebots/{typebotId}',
|
path: '/v1/typebots/{typebotId}',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Get a typebot',
|
summary: 'Get a typebot',
|
||||||
tags: ['Typebot'],
|
tags: ['Typebot'],
|
||||||
@@ -30,7 +31,7 @@ export const getTypebot = authenticatedProcedure
|
|||||||
.output(
|
.output(
|
||||||
z.object({
|
z.object({
|
||||||
typebot: typebotSchema,
|
typebot: typebotSchema,
|
||||||
isReadOnly: z.boolean(),
|
currentUserMode: z.enum(['guest', 'read', 'write']),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.query(
|
.query(
|
||||||
@@ -67,10 +68,7 @@ export const getTypebot = authenticatedProcedure
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
typebot: parsedTypebot,
|
typebot: parsedTypebot,
|
||||||
isReadOnly:
|
currentUserMode: getCurrentUserMode(user, existingTypebot),
|
||||||
existingTypebot.collaborators.find(
|
|
||||||
(collaborator) => collaborator.userId === user.id
|
|
||||||
)?.type === 'READ' ?? false,
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -81,3 +79,24 @@ export const getTypebot = authenticatedProcedure
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const getCurrentUserMode = (
|
||||||
|
user: { id: string } | undefined,
|
||||||
|
typebot: { collaborators: { userId: string; type: CollaborationType }[] } & {
|
||||||
|
workspace: { members: { userId: string }[] }
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const collaborator = typebot.collaborators.find((c) => c.userId === user?.id)
|
||||||
|
const isMemberOfWorkspace = typebot.workspace.members.some(
|
||||||
|
(m) => m.userId === user?.id
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
collaborator?.type === 'WRITE' ||
|
||||||
|
collaborator?.type === 'FULL_ACCESS' ||
|
||||||
|
isMemberOfWorkspace
|
||||||
|
)
|
||||||
|
return 'write'
|
||||||
|
|
||||||
|
if (collaborator) return 'read'
|
||||||
|
return 'guest'
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export const importTypebot = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/typebots/import',
|
path: '/v1/typebots/import',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Import a typebot',
|
summary: 'Import a typebot',
|
||||||
tags: ['Typebot'],
|
tags: ['Typebot'],
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const listTypebots = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/typebots',
|
path: '/v1/typebots',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'List typebots',
|
summary: 'List typebots',
|
||||||
tags: ['Typebot'],
|
tags: ['Typebot'],
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const publishTypebot = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/typebots/{typebotId}/publish',
|
path: '/v1/typebots/{typebotId}/publish',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Publish a typebot',
|
summary: 'Publish a typebot',
|
||||||
tags: ['Typebot'],
|
tags: ['Typebot'],
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const unpublishTypebot = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/typebots/{typebotId}/unpublish',
|
path: '/v1/typebots/{typebotId}/unpublish',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Unpublish a typebot',
|
summary: 'Unpublish a typebot',
|
||||||
tags: ['Typebot'],
|
tags: ['Typebot'],
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const updateTypebot = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
path: '/typebots/{typebotId}',
|
path: '/v1/typebots/{typebotId}',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Update a typebot',
|
summary: 'Update a typebot',
|
||||||
tags: ['Typebot'],
|
tags: ['Typebot'],
|
||||||
|
|||||||
@@ -4,18 +4,28 @@ import {
|
|||||||
User,
|
User,
|
||||||
Workspace,
|
Workspace,
|
||||||
MemberInWorkspace,
|
MemberInWorkspace,
|
||||||
|
Typebot,
|
||||||
} from '@typebot.io/prisma'
|
} from '@typebot.io/prisma'
|
||||||
|
import { settingsSchema } from '@typebot.io/schemas'
|
||||||
|
|
||||||
export const isReadTypebotForbidden = async (
|
export const isReadTypebotForbidden = async (
|
||||||
typebot: {
|
typebot: {
|
||||||
|
settings?: Typebot['settings']
|
||||||
collaborators: Pick<CollaboratorsOnTypebots, 'userId'>[]
|
collaborators: Pick<CollaboratorsOnTypebots, 'userId'>[]
|
||||||
} & {
|
} & {
|
||||||
workspace: Pick<Workspace, 'isSuspended' | 'isPastDue'> & {
|
workspace: Pick<Workspace, 'isSuspended' | 'isPastDue'> & {
|
||||||
members: Pick<MemberInWorkspace, 'userId'>[]
|
members: Pick<MemberInWorkspace, 'userId'>[]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
user: Pick<User, 'email' | 'id'>
|
user?: Pick<User, 'email' | 'id'>
|
||||||
) =>
|
) => {
|
||||||
|
const settings = typebot.settings
|
||||||
|
? settingsSchema.parse(typebot.settings)
|
||||||
|
: undefined
|
||||||
|
const isTypebotPublic = settings?.publicShare?.isEnabled === true
|
||||||
|
if (isTypebotPublic) return false
|
||||||
|
return (
|
||||||
|
!user ||
|
||||||
typebot.workspace.isSuspended ||
|
typebot.workspace.isSuspended ||
|
||||||
typebot.workspace.isPastDue ||
|
typebot.workspace.isPastDue ||
|
||||||
(env.ADMIN_EMAIL !== user.email &&
|
(env.ADMIN_EMAIL !== user.email &&
|
||||||
@@ -23,3 +33,5 @@ export const isReadTypebotForbidden = async (
|
|||||||
(collaborator) => collaborator.userId === user.id
|
(collaborator) => collaborator.userId === user.id
|
||||||
) &&
|
) &&
|
||||||
!typebot.workspace.members.some((member) => member.userId === user.id))
|
!typebot.workspace.members.some((member) => member.userId === user.id))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const sanitizeSettings = (
|
|||||||
mode: 'create' | 'update'
|
mode: 'create' | 'update'
|
||||||
): Typebot['settings'] => ({
|
): Typebot['settings'] => ({
|
||||||
...settings,
|
...settings,
|
||||||
|
publicShare: mode === 'create' ? undefined : settings.publicShare,
|
||||||
general:
|
general:
|
||||||
workspacePlan === Plan.FREE || settings.general
|
workspacePlan === Plan.FREE || settings.general
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const createWorkspace = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/workspaces',
|
path: '/v1/workspaces',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Create workspace',
|
summary: 'Create workspace',
|
||||||
tags: ['Workspace'],
|
tags: ['Workspace'],
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const deleteWorkspace = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
path: '/workspaces/{workspaceId}',
|
path: '/v1/workspaces/{workspaceId}',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Delete workspace',
|
summary: 'Delete workspace',
|
||||||
tags: ['Workspace'],
|
tags: ['Workspace'],
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const getWorkspace = authenticatedProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/workspaces/{workspaceId}',
|
path: '/v1/workspaces/{workspaceId}',
|
||||||
protect: true,
|
protect: true,
|
||||||
summary: 'Get workspace',
|
summary: 'Get workspace',
|
||||||
tags: ['Workspace'],
|
tags: ['Workspace'],
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user