✨ 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:*",
|
||||
"next-runtime-env": "1.6.2",
|
||||
"superjson": "1.12.4",
|
||||
"typescript": "5.2.2",
|
||||
"typescript": "5.3.2",
|
||||
"zod": "3.22.4"
|
||||
}
|
||||
}
|
||||
|
@ -143,7 +143,7 @@
|
||||
"editor.blocks.start.text": "Start",
|
||||
"editor.editableTypebotName.tooltip.rename.label": "Rename",
|
||||
"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.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",
|
||||
@ -153,7 +153,7 @@
|
||||
"editor.gettingStartedModal.seeAction.time": "5 minutes",
|
||||
"editor.headers.flowButton.label": "Flow",
|
||||
"editor.headers.helpButton.label": "Help",
|
||||
"editor.headers.previewButton.label": "Preview",
|
||||
"editor.headers.previewButton.label": "Test",
|
||||
"editor.headers.resultsButton.label": "Results",
|
||||
"editor.headers.savingSpinner.label": "Saving...",
|
||||
"editor.headers.settingsButton.label": "Settings",
|
||||
|
@ -4,6 +4,10 @@ import { ButtonProps, Button, useClipboard } from '@chakra-ui/react'
|
||||
interface CopyButtonProps extends ButtonProps {
|
||||
textToCopy: string
|
||||
onCopied?: () => void
|
||||
text?: {
|
||||
copy: string
|
||||
copied: string
|
||||
}
|
||||
}
|
||||
|
||||
export const CopyButton = (props: CopyButtonProps) => {
|
||||
@ -23,7 +27,7 @@ export const CopyButton = (props: CopyButtonProps) => {
|
||||
}}
|
||||
{...buttonProps}
|
||||
>
|
||||
{!hasCopied ? 'Copy' : 'Copied'}
|
||||
{!hasCopied ? props.text?.copy ?? 'Copy' : props.text?.copied ?? 'Copied'}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
@ -69,8 +69,12 @@ export const UserProvider = ({ children }: { children: ReactNode }) => {
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return
|
||||
if (status === 'loading') return
|
||||
const isSigningIn = () => ['/signin', '/register'].includes(router.pathname)
|
||||
if (!user && status === 'unauthenticated' && !isSigningIn())
|
||||
const isSignInPath = ['/signin', '/register'].includes(router.pathname)
|
||||
const isPathPublicFriendly = /\/typebots\/.+\/(edit|theme|settings)/.test(
|
||||
router.pathname
|
||||
)
|
||||
if (isSignInPath || isPathPublicFriendly) return
|
||||
if (!user && status === 'unauthenticated')
|
||||
router.replace({
|
||||
pathname: '/signin',
|
||||
query: {
|
||||
|
@ -11,7 +11,7 @@ export const getTotalAnswers = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/typebots/{typebotId}/analytics/totalAnswersInBlocks',
|
||||
path: '/v1/typebots/{typebotId}/analytics/totalAnswersInBlocks',
|
||||
protect: true,
|
||||
summary: 'List total answers in blocks',
|
||||
tags: ['Analytics'],
|
||||
|
@ -9,7 +9,7 @@ export const getTotalVisitedEdges = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/typebots/{typebotId}/analytics/totalVisitedEdges',
|
||||
path: '/v1/typebots/{typebotId}/analytics/totalVisitedEdges',
|
||||
protect: true,
|
||||
summary: 'List total edges used in results',
|
||||
tags: ['Analytics'],
|
||||
|
@ -20,7 +20,7 @@ const totalSteps = 5
|
||||
|
||||
export const OnboardingPage = () => {
|
||||
const { t } = useTranslate()
|
||||
const { push, replace } = useRouter()
|
||||
const { replace, query } = useRouter()
|
||||
const confettiCanvaContainer = useRef<HTMLCanvasElement | null>(null)
|
||||
const confettiCanon = useRef<confetti.CreateTypes>()
|
||||
const { user, updateUser } = useUser()
|
||||
@ -38,8 +38,8 @@ export const OnboardingPage = () => {
|
||||
useEffect(() => {
|
||||
if (!user?.createdAt) return
|
||||
if (isNewUser === false || !env.NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID)
|
||||
replace('/typebots')
|
||||
}, [isNewUser, replace, user?.createdAt])
|
||||
replace({ pathname: '/typebots', query })
|
||||
}, [isNewUser, query, replace, user?.createdAt])
|
||||
|
||||
const initConfettis = () => {
|
||||
if (!confettiCanvaContainer.current || confettiCanon.current) return
|
||||
@ -83,7 +83,7 @@ export const OnboardingPage = () => {
|
||||
right="5"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => push('/typebots')}
|
||||
onClick={() => replace({ pathname: '/typebots', query })}
|
||||
>
|
||||
{t('skip')}
|
||||
</Button>
|
||||
@ -95,7 +95,10 @@ export const OnboardingPage = () => {
|
||||
prefilledVariables={{ Name: user?.name, Email: user?.email }}
|
||||
onEnd={() => {
|
||||
setTimeout(() => {
|
||||
push('/typebots/create', { query: { isFirstBot: true } })
|
||||
replace({
|
||||
pathname: '/typebots',
|
||||
query: { ...query, isFirstBot: true },
|
||||
})
|
||||
}, 2000)
|
||||
}}
|
||||
onAnswer={updateUserInfo}
|
||||
|
@ -11,7 +11,7 @@ export const createCheckoutSession = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/billing/subscription/checkout',
|
||||
path: '/v1/billing/subscription/checkout',
|
||||
protect: true,
|
||||
summary: 'Create checkout session to create a new subscription',
|
||||
tags: ['Billing'],
|
||||
|
@ -11,7 +11,7 @@ export const createCustomCheckoutSession = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/billing/subscription/custom-checkout',
|
||||
path: '/v1/billing/subscription/custom-checkout',
|
||||
protect: true,
|
||||
summary:
|
||||
'Create custom checkout session to make a workspace pay for a custom plan',
|
||||
|
@ -10,7 +10,7 @@ export const getBillingPortalUrl = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/billing/subscription/portal',
|
||||
path: '/v1/billing/subscription/portal',
|
||||
protect: true,
|
||||
summary: 'Get Stripe billing portal URL',
|
||||
tags: ['Billing'],
|
||||
|
@ -11,7 +11,7 @@ export const getSubscription = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/billing/subscription',
|
||||
path: '/v1/billing/subscription',
|
||||
protect: true,
|
||||
summary: 'List invoices',
|
||||
tags: ['Billing'],
|
||||
|
@ -10,7 +10,7 @@ export const getUsage = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/billing/usage',
|
||||
path: '/v1/billing/usage',
|
||||
protect: true,
|
||||
summary: 'Get current plan usage',
|
||||
tags: ['Billing'],
|
||||
|
@ -12,7 +12,7 @@ export const listInvoices = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/billing/invoices',
|
||||
path: '/v1/billing/invoices',
|
||||
protect: true,
|
||||
summary: 'List invoices',
|
||||
tags: ['Billing'],
|
||||
|
@ -14,7 +14,7 @@ export const updateSubscription = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'PATCH',
|
||||
path: '/billing/subscription',
|
||||
path: '/v1/billing/subscription',
|
||||
protect: true,
|
||||
summary: 'Update subscription',
|
||||
tags: ['Billing'],
|
||||
|
@ -35,7 +35,7 @@ test('should work as expected', async ({ page }) => {
|
||||
'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(
|
||||
'src',
|
||||
RegExp(
|
||||
|
@ -44,7 +44,7 @@ test.describe.parallel('Embed bubble block', () => {
|
||||
])
|
||||
|
||||
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(
|
||||
'src',
|
||||
siteSrc
|
||||
|
@ -116,7 +116,7 @@ test.describe.parallel('Image bubble block', () => {
|
||||
])
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
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.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(
|
||||
'Bold text'
|
||||
)
|
||||
|
@ -51,7 +51,7 @@ test.describe.parallel('Video bubble block', () => {
|
||||
])
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
await expect(page.locator('video').nth(1)).toHaveAttribute(
|
||||
'src',
|
||||
videoSrc
|
||||
@ -75,7 +75,7 @@ test.describe.parallel('Video bubble block', () => {
|
||||
])
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
await expect(page.locator('iframe').nth(1)).toHaveAttribute(
|
||||
'src',
|
||||
'https://www.youtube.com/embed/dQw4w9WgXcQ'
|
||||
@ -99,7 +99,7 @@ test.describe.parallel('Video bubble block', () => {
|
||||
])
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
await expect(page.locator('iframe').nth(1)).toHaveAttribute(
|
||||
'src',
|
||||
'https://player.vimeo.com/video/649301125'
|
||||
|
@ -37,7 +37,7 @@ test.describe.parallel('Buttons input block', () => {
|
||||
await page.click('text=Delete')
|
||||
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 expect(page.getByRole('button', { name: 'Item 3' })).toBeHidden()
|
||||
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.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 1' }).click()
|
||||
@ -77,7 +77,7 @@ test('Variable buttons should work', async ({ page }) => {
|
||||
)
|
||||
|
||||
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 expect(page.getByTestId('guest-bubble')).toHaveText('Variable item')
|
||||
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.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
await expect(page.locator('[data-testid="from-date"]')).toHaveAttribute(
|
||||
'type',
|
||||
'date'
|
||||
|
@ -19,7 +19,7 @@ test.describe('Email input block', () => {
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
|
||||
await page.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
await expect(
|
||||
page.locator(
|
||||
`input[placeholder="${defaultEmailInputOptions.labels.placeholder}"]`
|
||||
|
@ -21,7 +21,7 @@ test('options should work', async ({ page }) => {
|
||||
|
||||
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="Skip"`)).toBeHidden()
|
||||
await page
|
||||
|
@ -19,7 +19,7 @@ test.describe('Number input block', () => {
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
|
||||
await page.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
await expect(
|
||||
page.locator(
|
||||
`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 expect(page.locator('text="Phone number:"')).toBeVisible()
|
||||
|
||||
await page.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
await stripePaymentForm(page)
|
||||
.locator(`[placeholder="1234 1234 1234 1234"]`)
|
||||
.fill('4000000000000002')
|
||||
|
@ -19,7 +19,7 @@ test.describe('Phone input block', () => {
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
|
||||
await page.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
await expect(
|
||||
page.locator(
|
||||
`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.getByLabel('Title:').fill('Third image')
|
||||
await page.getByLabel('Description:').fill('Third description')
|
||||
await page.getByRole('button', { name: 'Preview' }).click()
|
||||
await page.getByRole('button', { name: 'Test' }).click()
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
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.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
await expect(page.locator(`text=Send`)).toBeHidden()
|
||||
await page.getByRole('button', { name: '8' }).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.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
await expect(
|
||||
page.locator(
|
||||
`input[placeholder="${defaultTextInputOptions.labels.placeholder}"]`
|
||||
|
@ -19,7 +19,7 @@ test.describe('Url input block', () => {
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
|
||||
await page.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
await expect(
|
||||
page.locator(
|
||||
`input[placeholder="${defaultUrlInputOptions.labels.placeholder}"]`
|
||||
|
@ -34,7 +34,7 @@ test.describe('Chatwoot block', () => {
|
||||
await page.getByLabel('Email').fill('john@email.com')
|
||||
await page.getByLabel('Avatar URL').fill('https://domain.com/avatar.png')
|
||||
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(
|
||||
page.getByText('Chatwoot block is not supported in preview').nth(0)
|
||||
).toBeVisible()
|
||||
|
@ -31,7 +31,7 @@ test.describe.parallel('Google sheets integration', () => {
|
||||
'Georges'
|
||||
)
|
||||
|
||||
await page.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
await page
|
||||
.locator('typebot-standard')
|
||||
.locator('input[placeholder="Type your email..."]')
|
||||
@ -76,7 +76,7 @@ test.describe.parallel('Google sheets integration', () => {
|
||||
'Last name'
|
||||
)
|
||||
|
||||
await page.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
await page
|
||||
.locator('typebot-standard')
|
||||
.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 createNewVar(page, 'Last name')
|
||||
|
||||
await page.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
await page
|
||||
.locator('typebot-standard')
|
||||
.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('menuitem', { name: 'currency' }).click()
|
||||
await page.getByPlaceholder('Value').fill('USD')
|
||||
await page.getByRole('button', { name: 'Preview' }).click()
|
||||
await page.getByRole('button', { name: 'Test' }).click()
|
||||
await expect(
|
||||
page.getByText('Pixel is not enabled in Preview mode').nth(1)
|
||||
).toBeVisible()
|
||||
|
@ -59,7 +59,7 @@ test.describe('Send email block', () => {
|
||||
await page.click('text="Custom content?"')
|
||||
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 expect(
|
||||
page.locator('text=Emails are not sent in preview mode >> nth=0')
|
||||
|
@ -11,7 +11,7 @@ export const getResultExample = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/typebots/{typebotId}/webhookBlocks/{blockId}/getResultExample',
|
||||
path: '/v1/typebots/{typebotId}/webhookBlocks/{blockId}/getResultExample',
|
||||
protect: true,
|
||||
summary: 'Get result example',
|
||||
description:
|
||||
|
@ -12,7 +12,7 @@ export const listWebhookBlocks = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/typebots/{typebotId}/webhookBlocks',
|
||||
path: '/v1/typebots/{typebotId}/webhookBlocks',
|
||||
protect: true,
|
||||
summary: 'List webhook blocks',
|
||||
description:
|
||||
|
@ -10,7 +10,7 @@ export const subscribeWebhook = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/typebots/{typebotId}/webhookBlocks/{blockId}/subscribe',
|
||||
path: '/v1/typebots/{typebotId}/webhookBlocks/{blockId}/subscribe',
|
||||
protect: true,
|
||||
summary: 'Subscribe to webhook block',
|
||||
tags: ['Webhook'],
|
||||
|
@ -10,7 +10,7 @@ export const unsubscribeWebhook = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/typebots/{typebotId}/webhookBlocks/{blockId}/unsubscribe',
|
||||
path: '/v1/typebots/{typebotId}/webhookBlocks/{blockId}/unsubscribe',
|
||||
protect: true,
|
||||
summary: 'Unsubscribe from webhook block',
|
||||
tags: ['Webhook'],
|
||||
|
@ -20,7 +20,7 @@ export const ProjectsDropdown = ({
|
||||
const { workspace } = useWorkspace()
|
||||
const { showToast } = useToast()
|
||||
|
||||
const { data } = trpc.zemanticAi.listProjects.useQuery(
|
||||
const { data } = trpc.zemanticAI.listProjects.useQuery(
|
||||
{
|
||||
credentialsId,
|
||||
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 expect(page.getByText('A 100%')).toBeVisible()
|
||||
await expect(page.getByText('B 0%')).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Preview' }).click()
|
||||
await page.getByRole('button', { name: 'Test' }).click()
|
||||
await expect(
|
||||
page.locator('typebot-standard').getByText('How are you?')
|
||||
).toBeVisible()
|
||||
|
@ -49,7 +49,7 @@ test.describe('Condition block', () => {
|
||||
await page.click('button:has-text("Greater than")', { force: true })
|
||||
await page.fill('input[placeholder="Type a number..."]', '20')
|
||||
|
||||
await page.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
await page
|
||||
.locator('typebot-standard')
|
||||
.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.getByPlaceholder('Select a block').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.getByRole('button', { name: 'Send' }).click()
|
||||
await expect(
|
||||
|
@ -18,7 +18,7 @@ test.describe('Redirect block', () => {
|
||||
await page.click('text=Configure...')
|
||||
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 expect(page).toHaveURL('https://www.google.com')
|
||||
await page.goBack()
|
||||
@ -26,7 +26,7 @@ test.describe('Redirect block', () => {
|
||||
await page.click('text=Redirect to google.com')
|
||||
await page.click('text=Open in new tab')
|
||||
|
||||
await page.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
const [newPage] = await Promise.all([
|
||||
context.waitForEvent('page'),
|
||||
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"'
|
||||
)
|
||||
|
||||
await page.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
await page.getByRole('button', { name: 'Trigger code' }).click()
|
||||
await expect(page).toHaveURL('https://www.google.com')
|
||||
})
|
||||
|
@ -49,7 +49,7 @@ test.describe('Set variable block', () => {
|
||||
.getByRole('textbox')
|
||||
.fill('1000 + {{Total}}')
|
||||
|
||||
await page.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
await page
|
||||
.locator('typebot-standard')
|
||||
.locator('input[placeholder="Type a number..."]')
|
||||
|
@ -32,7 +32,7 @@ export const getLinkedTypebots = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/typebots/{typebotId}/linkedTypebots',
|
||||
path: '/v1/typebots/{typebotId}/linkedTypebots',
|
||||
protect: true,
|
||||
summary: 'Get linked typebots',
|
||||
tags: ['Typebot'],
|
||||
|
@ -35,7 +35,7 @@ test('should be configurable', async ({ page }) => {
|
||||
await page.click('input[placeholder="Select a group"]')
|
||||
await page.click('text=Group #2')
|
||||
|
||||
await page.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
await expect(
|
||||
page.locator('typebot-standard').locator('text=Second block')
|
||||
).toBeVisible()
|
||||
@ -45,7 +45,7 @@ test('should be configurable', async ({ page }) => {
|
||||
await page.getByTestId('selected-item-label').nth(1).click({ force: true })
|
||||
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').press('Enter')
|
||||
await expect(
|
||||
@ -60,7 +60,7 @@ test('should be configurable', async ({ page }) => {
|
||||
await page.getByRole('textbox').nth(1).click()
|
||||
await page.click('button >> text=Hello')
|
||||
|
||||
await page.click('text=Preview')
|
||||
await page.click('text=Test')
|
||||
await expect(
|
||||
page.locator('typebot-standard').locator('text=Hello world')
|
||||
).toBeVisible()
|
||||
|
@ -15,7 +15,7 @@ test.describe('Wait block', () => {
|
||||
await page.click('text=Configure...')
|
||||
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.waitForTimeout(1000)
|
||||
await expect(
|
||||
|
@ -9,7 +9,7 @@ export const getCollaborators = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/typebots/{typebotId}/collaborators',
|
||||
path: '/v1/typebots/{typebotId}/collaborators',
|
||||
protect: true,
|
||||
summary: 'Get collaborators',
|
||||
tags: ['Collaborators'],
|
||||
|
@ -38,7 +38,7 @@ test.describe('Typebot owner', () => {
|
||||
},
|
||||
])
|
||||
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 page.fill(
|
||||
'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=Guest folder')).toBeHidden()
|
||||
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 expect(page.locator('text="Remove"')).toBeHidden()
|
||||
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=Guest folder')).toBeHidden()
|
||||
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 expect(page.locator('text="Remove"')).toBeHidden()
|
||||
await expect(page.locator('text=John Doe')).toBeVisible()
|
||||
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 (
|
||||
<Stack spacing={1} pt="4" pb="2">
|
||||
<Stack spacing={1} pt="4">
|
||||
<HStack as="form" onSubmit={handleInvitationSubmit} px="4" pb="2">
|
||||
<Input
|
||||
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({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/credentials',
|
||||
path: '/v1/credentials',
|
||||
protect: true,
|
||||
summary: 'Create credentials',
|
||||
tags: ['Credentials'],
|
||||
|
@ -8,7 +8,7 @@ export const deleteCredentials = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'DELETE',
|
||||
path: '/credentials/:credentialsId',
|
||||
path: '/v1/credentials/:credentialsId',
|
||||
protect: true,
|
||||
summary: 'Delete credentials',
|
||||
tags: ['Credentials'],
|
||||
|
@ -16,7 +16,7 @@ export const listCredentials = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/credentials',
|
||||
path: '/v1/credentials',
|
||||
protect: true,
|
||||
summary: 'List workspace credentials',
|
||||
tags: ['Credentials'],
|
||||
|
@ -11,7 +11,7 @@ export const createCustomDomain = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/custom-domains',
|
||||
path: '/v1/custom-domains',
|
||||
protect: true,
|
||||
summary: 'Create custom domain',
|
||||
tags: ['Custom domains'],
|
||||
|
@ -10,7 +10,7 @@ export const deleteCustomDomain = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'DELETE',
|
||||
path: '/custom-domains/{name}',
|
||||
path: '/v1/custom-domains/{name}',
|
||||
protect: true,
|
||||
summary: 'Delete custom domain',
|
||||
tags: ['Custom domains'],
|
||||
|
@ -9,7 +9,7 @@ export const listCustomDomains = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/custom-domains',
|
||||
path: '/v1/custom-domains',
|
||||
protect: true,
|
||||
summary: 'List custom domains',
|
||||
tags: ['Custom domains'],
|
||||
|
@ -16,7 +16,7 @@ export const verifyCustomDomain = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/custom-domains/{name}/verify',
|
||||
path: '/v1/custom-domains/{name}/verify',
|
||||
protect: true,
|
||||
summary: 'Verify domain config',
|
||||
tags: ['Custom domains'],
|
||||
|
@ -16,10 +16,17 @@ import { GraphDndProvider } from '@/features/graph/providers/GraphDndProvider'
|
||||
import { GraphProvider } from '@/features/graph/providers/GraphProvider'
|
||||
import { GroupsCoordinatesProvider } from '@/features/graph/providers/GroupsCoordinateProvider'
|
||||
import { EventsCoordinatesProvider } from '@/features/graph/providers/EventsCoordinateProvider'
|
||||
import { TypebotNotFoundPage } from './TypebotNotFoundPage'
|
||||
|
||||
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 (
|
||||
<EditorProvider>
|
||||
<Seo title={typebot?.name ? `${typebot.name} | Editor` : 'Editor'} />
|
||||
@ -30,18 +37,19 @@ export const EditorPage = () => {
|
||||
flex="1"
|
||||
pos="relative"
|
||||
h="full"
|
||||
bgColor={useColorModeValue('#f4f5f8', 'gray.850')}
|
||||
backgroundImage={useColorModeValue(
|
||||
'radial-gradient(#c6d0e1 1px, transparent 0)',
|
||||
'radial-gradient(#2f2f39 1px, transparent 0)'
|
||||
)}
|
||||
bgColor={bgColor}
|
||||
backgroundImage={backgroundImage}
|
||||
backgroundSize="40px 40px"
|
||||
backgroundPosition="-19px -19px"
|
||||
>
|
||||
{typebot ? (
|
||||
<GraphDndProvider>
|
||||
{!isReadOnly && <BlocksSideBar />}
|
||||
<GraphProvider isReadOnly={isReadOnly}>
|
||||
{currentUserMode === 'write' && <BlocksSideBar />}
|
||||
<GraphProvider
|
||||
isReadOnly={
|
||||
currentUserMode === 'read' || currentUserMode === 'guest'
|
||||
}
|
||||
>
|
||||
<GroupsCoordinatesProvider groups={typebot.groups}>
|
||||
<EventsCoordinatesProvider events={typebot.events}>
|
||||
<Graph flex="1" typebot={typebot} key={typebot.id} />
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
import {
|
||||
BuoyIcon,
|
||||
ChevronLeftIcon,
|
||||
PlayIcon,
|
||||
RedoIcon,
|
||||
UndoIcon,
|
||||
} from '@/components/icons'
|
||||
@ -23,7 +24,7 @@ import Link from 'next/link'
|
||||
import { EditableEmojiOrImageIcon } from '@/components/EditableEmojiOrImageIcon'
|
||||
import { useUndoShortcut } from '@/hooks/useUndoShortcut'
|
||||
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 { headerHeight } from '../constants'
|
||||
import { RightPanel, useEditor } from '../providers/EditorProvider'
|
||||
@ -31,6 +32,7 @@ import { useTypebot } from '../providers/TypebotProvider'
|
||||
import { SupportBubble } from '@/components/SupportBubble'
|
||||
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
|
||||
import { useTranslate } from '@tolgee/react'
|
||||
import { GuestTypebotHeader } from './UnauthenticatedTypebotHeader'
|
||||
|
||||
export const TypebotHeader = () => {
|
||||
const { t } = useTranslate()
|
||||
@ -45,6 +47,7 @@ export const TypebotHeader = () => {
|
||||
canUndo,
|
||||
canRedo,
|
||||
isSavingLoading,
|
||||
currentUserMode,
|
||||
} = useTypebot()
|
||||
const {
|
||||
setRightPanel,
|
||||
@ -58,6 +61,7 @@ export const TypebotHeader = () => {
|
||||
setUndoShortcutTooltipOpen(false)
|
||||
}, 1000)
|
||||
const { isOpen, onOpen } = useDisclosure()
|
||||
const headerBgColor = useColorModeValue('white', 'gray.900')
|
||||
|
||||
const handleNameSubmit = (name: string) =>
|
||||
updateTypebot({ updates: { name } })
|
||||
@ -86,6 +90,7 @@ export const TypebotHeader = () => {
|
||||
: window.open('https://docs.typebot.io', '_blank')
|
||||
}
|
||||
|
||||
if (currentUserMode === 'guest') return <GuestTypebotHeader />
|
||||
return (
|
||||
<Flex
|
||||
w="full"
|
||||
@ -95,7 +100,7 @@ export const TypebotHeader = () => {
|
||||
h={`${headerHeight}px`}
|
||||
zIndex={100}
|
||||
pos="relative"
|
||||
bgColor={useColorModeValue('white', 'gray.900')}
|
||||
bgColor={headerBgColor}
|
||||
flexShrink={0}
|
||||
>
|
||||
{isOpen && <SupportBubble autoShowDelay={0} />}
|
||||
@ -203,33 +208,35 @@ export const TypebotHeader = () => {
|
||||
)
|
||||
</HStack>
|
||||
|
||||
<HStack>
|
||||
<Tooltip
|
||||
label={isUndoShortcutTooltipOpen ? 'Changes reverted!' : 'Undo'}
|
||||
isOpen={isUndoShortcutTooltipOpen ? true : undefined}
|
||||
hasArrow={isUndoShortcutTooltipOpen}
|
||||
>
|
||||
<IconButton
|
||||
display={['none', 'flex']}
|
||||
icon={<UndoIcon />}
|
||||
size="sm"
|
||||
aria-label="Undo"
|
||||
onClick={undo}
|
||||
isDisabled={!canUndo}
|
||||
/>
|
||||
</Tooltip>
|
||||
{currentUserMode === 'write' && (
|
||||
<HStack>
|
||||
<Tooltip
|
||||
label={isUndoShortcutTooltipOpen ? 'Changes reverted!' : 'Undo'}
|
||||
isOpen={isUndoShortcutTooltipOpen ? true : undefined}
|
||||
hasArrow={isUndoShortcutTooltipOpen}
|
||||
>
|
||||
<IconButton
|
||||
display={['none', 'flex']}
|
||||
icon={<UndoIcon />}
|
||||
size="sm"
|
||||
aria-label="Undo"
|
||||
onClick={undo}
|
||||
isDisabled={!canUndo}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Redo">
|
||||
<IconButton
|
||||
display={['none', 'flex']}
|
||||
icon={<RedoIcon />}
|
||||
size="sm"
|
||||
aria-label="Redo"
|
||||
onClick={redo}
|
||||
isDisabled={!canRedo}
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
<Tooltip label="Redo">
|
||||
<IconButton
|
||||
display={['none', 'flex']}
|
||||
icon={<RedoIcon />}
|
||||
size="sm"
|
||||
aria-label="Redo"
|
||||
onClick={redo}
|
||||
isDisabled={!canRedo}
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
)}
|
||||
<Button leftIcon={<BuoyIcon />} onClick={handleHelpClick} size="sm">
|
||||
{t('editor.headers.helpButton.label')}
|
||||
</Button>
|
||||
@ -246,19 +253,20 @@ export const TypebotHeader = () => {
|
||||
|
||||
<HStack right="40px" pos="absolute" display={['none', 'flex']}>
|
||||
<Flex pos="relative">
|
||||
<CollaborationMenuButton isLoading={isNotDefined(typebot)} />
|
||||
<ShareTypebotButton isLoading={isNotDefined(typebot)} />
|
||||
</Flex>
|
||||
{router.pathname.includes('/edit') && isNotDefined(rightPanel) && (
|
||||
<Button
|
||||
colorScheme="gray"
|
||||
onClick={handlePreviewClick}
|
||||
isLoading={isNotDefined(typebot)}
|
||||
leftIcon={<PlayIcon />}
|
||||
size="sm"
|
||||
>
|
||||
{t('editor.headers.previewButton.label')}
|
||||
</Button>
|
||||
)}
|
||||
<PublishButton size="sm" />
|
||||
{currentUserMode === 'write' && <PublishButton size="sm" />}
|
||||
</HStack>
|
||||
</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"')
|
||||
).toBeVisible()
|
||||
await page.click('[aria-label="Close"]')
|
||||
await page.click('text="Preview"')
|
||||
await page.click('text="Test"')
|
||||
await expect(
|
||||
page.locator('typebot-standard').locator('text="Hello this is group 1"')
|
||||
).toBeVisible()
|
||||
|
@ -23,8 +23,11 @@ const initialState = {
|
||||
future: [],
|
||||
}
|
||||
|
||||
type Params = { isReadOnly?: boolean }
|
||||
|
||||
export const useUndo = <T extends { updatedAt: Date }>(
|
||||
initialPresent?: T
|
||||
initialPresent?: T,
|
||||
params?: Params
|
||||
): [T | undefined, Actions<T>] => {
|
||||
const [history, setHistory] = useState<History<T>>(initialState)
|
||||
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 undo = useCallback(() => {
|
||||
if (params?.isReadOnly) return
|
||||
const { past, present, future } = history
|
||||
if (past.length === 0 || !present) return
|
||||
|
||||
@ -47,9 +51,10 @@ export const useUndo = <T extends { updatedAt: Date }>(
|
||||
future: [present, ...future],
|
||||
})
|
||||
presentRef.current = newPresent
|
||||
}, [history])
|
||||
}, [history, params?.isReadOnly])
|
||||
|
||||
const redo = useCallback(() => {
|
||||
if (params?.isReadOnly) return
|
||||
const { past, present, future } = history
|
||||
if (future.length === 0) return
|
||||
const next = future[0]
|
||||
@ -61,11 +66,12 @@ export const useUndo = <T extends { updatedAt: Date }>(
|
||||
future: newFuture,
|
||||
})
|
||||
presentRef.current = next
|
||||
}, [history])
|
||||
}, [history, params?.isReadOnly])
|
||||
|
||||
const set = useCallback(
|
||||
(newPresentArg: T | ((current: T) => T) | undefined) => {
|
||||
const { past, present } = history
|
||||
if (isDefined(present) && params?.isReadOnly) return
|
||||
const newPresent =
|
||||
typeof newPresentArg === 'function'
|
||||
? newPresentArg(presentRef.current as T)
|
||||
@ -92,16 +98,17 @@ export const useUndo = <T extends { updatedAt: Date }>(
|
||||
})
|
||||
presentRef.current = newPresent
|
||||
},
|
||||
[history]
|
||||
[history, params?.isReadOnly]
|
||||
)
|
||||
|
||||
const flush = useCallback(() => {
|
||||
if (params?.isReadOnly) return
|
||||
setHistory({
|
||||
present: presentRef.current ?? undefined,
|
||||
past: [],
|
||||
future: [],
|
||||
})
|
||||
}, [])
|
||||
}, [params?.isReadOnly])
|
||||
|
||||
return [history.present, { set, undo, redo, flush, canUndo, canRedo }]
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { isDefined, omit } from '@typebot.io/lib'
|
||||
import { edgesAction, EdgesActions } from './typebotActions/edges'
|
||||
@ -52,7 +53,8 @@ const typebotContext = createContext<
|
||||
typebot?: TypebotV6
|
||||
publishedTypebot?: PublicTypebotV6
|
||||
publishedTypebotVersion?: PublicTypebot['version']
|
||||
isReadOnly?: boolean
|
||||
currentUserMode: 'guest' | 'read' | 'write'
|
||||
is404: boolean
|
||||
isPublished: boolean
|
||||
isSavingLoading: boolean
|
||||
save: () => Promise<TypebotV6 | undefined>
|
||||
@ -84,6 +86,7 @@ export const TypebotProvider = ({
|
||||
}) => {
|
||||
const { push } = useRouter()
|
||||
const { showToast } = useToast()
|
||||
const [is404, setIs404] = useState(false)
|
||||
|
||||
const {
|
||||
data: typebotData,
|
||||
@ -96,13 +99,10 @@ export const TypebotProvider = ({
|
||||
retry: 0,
|
||||
onError: (error) => {
|
||||
if (error.data?.httpStatus === 404) {
|
||||
showToast({
|
||||
status: 'info',
|
||||
description: "Couldn't find typebot. Redirecting...",
|
||||
})
|
||||
push('/typebots')
|
||||
setIs404(true)
|
||||
return
|
||||
}
|
||||
setIs404(false)
|
||||
showToast({
|
||||
title: 'Could not fetch typebot',
|
||||
description: error.message,
|
||||
@ -112,6 +112,9 @@ export const TypebotProvider = ({
|
||||
},
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
setIs404(false)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@ -119,7 +122,10 @@ export const TypebotProvider = ({
|
||||
trpc.typebot.getPublishedTypebot.useQuery(
|
||||
{ typebotId: typebotId as string, migrateToLatestVersion: true },
|
||||
{
|
||||
enabled: isDefined(typebotId),
|
||||
enabled:
|
||||
isDefined(typebotId) &&
|
||||
(typebotData?.currentUserMode === 'read' ||
|
||||
typebotData?.currentUserMode === 'write'),
|
||||
onError: (error) => {
|
||||
showToast({
|
||||
title: 'Could not fetch published typebot',
|
||||
@ -153,11 +159,16 @@ export const TypebotProvider = ({
|
||||
const typebot = typebotData?.typebot as TypebotV6
|
||||
const publishedTypebot = (publishedTypebotData?.publishedTypebot ??
|
||||
undefined) as PublicTypebotV6 | undefined
|
||||
const isReadOnly = ['read', 'guest'].includes(
|
||||
typebotData?.currentUserMode ?? 'guest'
|
||||
)
|
||||
|
||||
const [
|
||||
localTypebot,
|
||||
{ redo, undo, flush, canRedo, canUndo, set: setLocalTypebot },
|
||||
] = useUndo<TypebotV6>(undefined)
|
||||
] = useUndo<TypebotV6>(undefined, {
|
||||
isReadOnly,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!typebot && isDefined(localTypebot)) setLocalTypebot(undefined)
|
||||
@ -182,7 +193,7 @@ export const TypebotProvider = ({
|
||||
|
||||
const saveTypebot = useCallback(
|
||||
async (updates?: Partial<TypebotV6>) => {
|
||||
if (!localTypebot || !typebot || typebotData?.isReadOnly) return
|
||||
if (!localTypebot || !typebot || isReadOnly) return
|
||||
const typebotToSave = { ...localTypebot, ...updates }
|
||||
if (dequal(omit(typebot, 'updatedAt'), omit(typebotToSave, 'updatedAt')))
|
||||
return
|
||||
@ -194,13 +205,7 @@ export const TypebotProvider = ({
|
||||
setLocalTypebot({ ...newTypebot })
|
||||
return newTypebot
|
||||
},
|
||||
[
|
||||
localTypebot,
|
||||
setLocalTypebot,
|
||||
typebot,
|
||||
typebotData?.isReadOnly,
|
||||
updateTypebot,
|
||||
]
|
||||
[isReadOnly, localTypebot, setLocalTypebot, typebot, updateTypebot]
|
||||
)
|
||||
|
||||
useAutoSave(
|
||||
@ -232,7 +237,7 @@ export const TypebotProvider = ({
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!localTypebot || !typebot || typebotData?.isReadOnly) return
|
||||
if (!localTypebot || !typebot || isReadOnly) return
|
||||
if (!areTypebotsEqual(localTypebot, typebot)) {
|
||||
window.addEventListener('beforeunload', preventUserFromRefreshing)
|
||||
}
|
||||
@ -240,7 +245,7 @@ export const TypebotProvider = ({
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||
}
|
||||
}, [localTypebot, typebot, typebotData?.isReadOnly])
|
||||
}, [localTypebot, typebot, isReadOnly])
|
||||
|
||||
const updateLocalTypebot = async ({
|
||||
updates,
|
||||
@ -249,7 +254,7 @@ export const TypebotProvider = ({
|
||||
updates: UpdateTypebotPayload
|
||||
save?: boolean
|
||||
}) => {
|
||||
if (!localTypebot) return
|
||||
if (!localTypebot || isReadOnly) return
|
||||
const newTypebot = { ...localTypebot, ...updates }
|
||||
setLocalTypebot(newTypebot)
|
||||
if (save) await saveTypebot(newTypebot)
|
||||
@ -269,8 +274,9 @@ export const TypebotProvider = ({
|
||||
typebot: localTypebot,
|
||||
publishedTypebot,
|
||||
publishedTypebotVersion: publishedTypebotData?.version,
|
||||
isReadOnly: typebotData?.isReadOnly,
|
||||
currentUserMode: typebotData?.currentUserMode ?? 'guest',
|
||||
isSavingLoading: isSaving,
|
||||
is404,
|
||||
save: saveTypebot,
|
||||
undo,
|
||||
redo,
|
||||
|
@ -151,7 +151,7 @@ export const BlockNode = ({
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
setFocusedGroupId(groupId)
|
||||
e.stopPropagation()
|
||||
if (isTextBubbleBlock(block)) setIsEditing(true)
|
||||
if (isTextBubbleBlock(block) && !isReadOnly) setIsEditing(true)
|
||||
setOpenedBlockId(block.id)
|
||||
}
|
||||
|
||||
|
@ -112,11 +112,7 @@ export const BlockNodesList = ({ blocks, groupIndex, groupRef }: Props) => {
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack
|
||||
spacing={1}
|
||||
transition="none"
|
||||
pointerEvents={isReadOnly ? 'none' : 'auto'}
|
||||
>
|
||||
<Stack spacing={1} transition="none">
|
||||
<PlaceholderNode
|
||||
isVisible={showSortPlaceholders}
|
||||
isExpanded={expandedPlaceholderIndex === 0}
|
||||
|
@ -227,7 +227,6 @@ const NonMemoizedDraggableGroupNode = ({
|
||||
onChange={setGroupTitle}
|
||||
onSubmit={handleTitleSubmit}
|
||||
fontWeight="semibold"
|
||||
pointerEvents={isReadOnly ? 'none' : 'auto'}
|
||||
pr="8"
|
||||
>
|
||||
<EditablePreview
|
||||
|
@ -27,11 +27,12 @@ import { parseDefaultPublicId } from '../helpers/parseDefaultPublicId'
|
||||
import { useTranslate } from '@tolgee/react'
|
||||
import { env } from '@typebot.io/env'
|
||||
import DomainStatusIcon from '@/features/customDomains/components/DomainStatusIcon'
|
||||
import { TypebotNotFoundPage } from '@/features/editor/components/TypebotNotFoundPage'
|
||||
|
||||
export const SharePage = () => {
|
||||
const { t } = useTranslate()
|
||||
const { workspace } = useWorkspace()
|
||||
const { typebot, updateTypebot, publishedTypebot } = useTypebot()
|
||||
const { typebot, updateTypebot, publishedTypebot, is404 } = useTypebot()
|
||||
const { showToast } = useToast()
|
||||
|
||||
const handlePublicIdChange = async (publicId: string) => {
|
||||
@ -87,6 +88,7 @@ export const SharePage = () => {
|
||||
return true
|
||||
}
|
||||
|
||||
if (is404) return <TypebotNotFoundPage />
|
||||
return (
|
||||
<Flex flexDir="column" pb="40">
|
||||
<Seo title={typebot?.name ? `${typebot.name} | Share` : 'Share'} />
|
||||
|
@ -10,7 +10,7 @@ export const deleteResults = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'DELETE',
|
||||
path: '/typebots/{typebotId}/results',
|
||||
path: '/v1/typebots/{typebotId}/results',
|
||||
protect: true,
|
||||
summary: 'Delete results',
|
||||
tags: ['Results'],
|
||||
|
@ -9,7 +9,7 @@ export const getResult = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/typebots/{typebotId}/results/{resultId}',
|
||||
path: '/v1/typebots/{typebotId}/results/{resultId}',
|
||||
protect: true,
|
||||
summary: 'Get result by id',
|
||||
tags: ['Results'],
|
||||
|
@ -8,7 +8,7 @@ export const getResultLogs = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/typebots/{typebotId}/results/{resultId}/logs',
|
||||
path: '/v1/typebots/{typebotId}/results/{resultId}/logs',
|
||||
protect: true,
|
||||
summary: 'List result logs',
|
||||
tags: ['Results'],
|
||||
|
@ -11,7 +11,7 @@ export const getResults = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/typebots/{typebotId}/results',
|
||||
path: '/v1/typebots/{typebotId}/results',
|
||||
protect: true,
|
||||
summary: 'List results ordered by descending creation date',
|
||||
tags: ['Results'],
|
||||
|
@ -18,15 +18,20 @@ import { useMemo } from 'react'
|
||||
import { useStats } from '../hooks/useStats'
|
||||
import { ResultsProvider } from '../ResultsProvider'
|
||||
import { ResultsTableContainer } from './ResultsTableContainer'
|
||||
import { TypebotNotFoundPage } from '@/features/editor/components/TypebotNotFoundPage'
|
||||
|
||||
export const ResultsPage = () => {
|
||||
const router = useRouter()
|
||||
const { workspace } = useWorkspace()
|
||||
const { typebot, publishedTypebot } = useTypebot()
|
||||
const { typebot, publishedTypebot, is404 } = useTypebot()
|
||||
const isAnalytics = useMemo(
|
||||
() => router.pathname.endsWith('analytics'),
|
||||
[router.pathname]
|
||||
)
|
||||
const bgColor = useColorModeValue(
|
||||
router.pathname.endsWith('analytics') ? '#f4f5f8' : 'white',
|
||||
router.pathname.endsWith('analytics') ? 'gray.850' : 'gray.900'
|
||||
)
|
||||
const { showToast } = useToast()
|
||||
|
||||
const { stats, mutate } = useStats({
|
||||
@ -41,6 +46,7 @@ export const ResultsPage = () => {
|
||||
})
|
||||
}
|
||||
|
||||
if (is404) return <TypebotNotFoundPage />
|
||||
return (
|
||||
<Flex overflow="hidden" h="100vh" flexDir="column">
|
||||
<Seo
|
||||
@ -55,14 +61,7 @@ export const ResultsPage = () => {
|
||||
}
|
||||
/>
|
||||
<TypebotHeader />
|
||||
<Flex
|
||||
h="full"
|
||||
w="full"
|
||||
bgColor={useColorModeValue(
|
||||
router.pathname.endsWith('analytics') ? '#f4f5f8' : 'white',
|
||||
router.pathname.endsWith('analytics') ? 'gray.850' : 'gray.900'
|
||||
)}
|
||||
>
|
||||
<Flex h="full" w="full" bgColor={bgColor}>
|
||||
<Flex
|
||||
pos="absolute"
|
||||
zIndex={2}
|
||||
|
@ -48,7 +48,7 @@ export const ResultsTable = ({
|
||||
onResultExpandIndex,
|
||||
}: ResultsTableProps) => {
|
||||
const background = useColorModeValue('white', colors.gray[900])
|
||||
const { updateTypebot, isReadOnly } = useTypebot()
|
||||
const { updateTypebot, currentUserMode } = useTypebot()
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({})
|
||||
const [isTableScrolled, setIsTableScrolled] = useState(false)
|
||||
const bottomElement = useRef<HTMLDivElement | null>(null)
|
||||
@ -212,7 +212,7 @@ export const ResultsTable = ({
|
||||
return (
|
||||
<Stack maxW="1600px" px="4" overflowY="hidden" spacing={6}>
|
||||
<HStack w="full" justifyContent="flex-end">
|
||||
{isReadOnly ? null : (
|
||||
{currentUserMode === 'write' && (
|
||||
<SelectionToolbar
|
||||
selectedResultsId={Object.keys(rowSelection)}
|
||||
onClearSelection={() => setRowSelection({})}
|
||||
|
@ -5,10 +5,12 @@ import { getViewerUrl } from '@typebot.io/lib/getViewerUrl'
|
||||
import { SettingsSideMenu } from './SettingsSideMenu'
|
||||
import { TypebotHeader } from '@/features/editor/components/TypebotHeader'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { TypebotNotFoundPage } from '@/features/editor/components/TypebotNotFoundPage'
|
||||
|
||||
export const SettingsPage = () => {
|
||||
const { typebot } = useTypebot()
|
||||
const { typebot, is404 } = useTypebot()
|
||||
|
||||
if (is404) return <TypebotNotFoundPage />
|
||||
return (
|
||||
<Flex overflow="hidden" h="100vh" flexDir="column">
|
||||
<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 { 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
|
||||
.meta({
|
||||
openapi: {
|
||||
|
@ -10,7 +10,7 @@ export const deleteThemeTemplate = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'DELETE',
|
||||
path: '/themeTemplates/{themeTemplateId}',
|
||||
path: '/v1/themeTemplates/{themeTemplateId}',
|
||||
protect: true,
|
||||
summary: 'Delete a theme template',
|
||||
tags: ['Theme template'],
|
||||
|
@ -10,7 +10,7 @@ export const listThemeTemplates = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/themeTemplates',
|
||||
path: '/v1/themeTemplates',
|
||||
protect: true,
|
||||
summary: 'List theme templates',
|
||||
tags: ['Theme template'],
|
||||
|
@ -10,7 +10,7 @@ export const saveThemeTemplate = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'PUT',
|
||||
path: '/themeTemplates/{themeTemplateId}',
|
||||
path: '/v1/themeTemplates/{themeTemplateId}',
|
||||
protect: true,
|
||||
summary: 'Save theme template',
|
||||
tags: ['Theme template'],
|
||||
|
@ -4,10 +4,12 @@ import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { Flex } from '@chakra-ui/react'
|
||||
import { Standard } from '@typebot.io/nextjs'
|
||||
import { ThemeSideMenu } from './ThemeSideMenu'
|
||||
import { TypebotNotFoundPage } from '@/features/editor/components/TypebotNotFoundPage'
|
||||
|
||||
export const ThemePage = () => {
|
||||
const { typebot } = useTypebot()
|
||||
const { typebot, is404 } = useTypebot()
|
||||
|
||||
if (is404) return <TypebotNotFoundPage />
|
||||
return (
|
||||
<Flex overflow="hidden" h="100vh" flexDir="column">
|
||||
<Seo title={typebot?.name ? `${typebot.name} | Theme` : 'Theme'} />
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
Heading,
|
||||
HStack,
|
||||
Stack,
|
||||
Tag,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChatIcon, CodeIcon, DropletIcon, TableIcon } from '@/components/icons'
|
||||
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'
|
||||
|
||||
export const ThemeSideMenu = () => {
|
||||
const { typebot, updateTypebot } = useTypebot()
|
||||
const { typebot, updateTypebot, currentUserMode } = useTypebot()
|
||||
|
||||
const updateChatTheme = (chat: ChatTheme) =>
|
||||
typebot && updateTypebot({ updates: { theme: { ...typebot.theme, chat } } })
|
||||
@ -71,31 +70,29 @@ export const ThemeSideMenu = () => {
|
||||
Customize the theme
|
||||
</Heading>
|
||||
<Accordion allowMultiple>
|
||||
<AccordionItem>
|
||||
<AccordionButton py={6}>
|
||||
<HStack flex="1" pl={2}>
|
||||
<TableIcon />
|
||||
<Heading fontSize="lg">
|
||||
<HStack>
|
||||
<span>Templates</span> <Tag colorScheme="orange">New!</Tag>
|
||||
</HStack>
|
||||
</Heading>
|
||||
</HStack>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={12}>
|
||||
{typebot && (
|
||||
<ThemeTemplates
|
||||
selectedTemplateId={
|
||||
typebot.selectedThemeTemplateId ?? undefined
|
||||
}
|
||||
currentTheme={typebot.theme}
|
||||
workspaceId={typebot.workspaceId}
|
||||
onTemplateSelect={selectedTemplate}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
{currentUserMode === 'write' && (
|
||||
<AccordionItem>
|
||||
<AccordionButton py={6}>
|
||||
<HStack flex="1" pl={2}>
|
||||
<TableIcon />
|
||||
<Heading fontSize="lg">Templates</Heading>
|
||||
</HStack>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={12}>
|
||||
{typebot && (
|
||||
<ThemeTemplates
|
||||
selectedTemplateId={
|
||||
typebot.selectedThemeTemplateId ?? undefined
|
||||
}
|
||||
currentTheme={typebot.theme}
|
||||
workspaceId={typebot.workspaceId}
|
||||
onTemplateSelect={selectedTemplate}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
<AccordionItem>
|
||||
<AccordionButton py={6}>
|
||||
<HStack flex="1" pl={2}>
|
||||
|
@ -233,7 +233,7 @@ test.describe.parallel('Theme page', () => {
|
||||
})
|
||||
await page.goto(`/typebots/${typebotId}/theme`)
|
||||
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.getByPlaceholder('My template').fill('My awesome theme')
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
|
@ -35,7 +35,7 @@ export const createTypebot = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/typebots',
|
||||
path: '/v1/typebots',
|
||||
protect: true,
|
||||
summary: 'Create a typebot',
|
||||
tags: ['Typebot'],
|
||||
|
@ -10,7 +10,7 @@ export const deleteTypebot = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'DELETE',
|
||||
path: '/typebots/{typebotId}',
|
||||
path: '/v1/typebots/{typebotId}',
|
||||
protect: true,
|
||||
summary: 'Delete a typebot',
|
||||
tags: ['Typebot'],
|
||||
|
@ -15,7 +15,7 @@ export const getPublishedTypebot = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/typebots/{typebotId}/publishedTypebot',
|
||||
path: '/v1/typebots/{typebotId}/publishedTypebot',
|
||||
protect: true,
|
||||
summary: 'Get published typebot',
|
||||
tags: ['Typebot'],
|
||||
|
@ -1,16 +1,17 @@
|
||||
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 { typebotSchema } from '@typebot.io/schemas'
|
||||
import { z } from 'zod'
|
||||
import { isReadTypebotForbidden } from '../helpers/isReadTypebotForbidden'
|
||||
import { migrateTypebot } from '@typebot.io/lib/migrations/migrateTypebot'
|
||||
import { CollaborationType } from '@typebot.io/prisma'
|
||||
|
||||
export const getTypebot = authenticatedProcedure
|
||||
export const getTypebot = publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/typebots/{typebotId}',
|
||||
path: '/v1/typebots/{typebotId}',
|
||||
protect: true,
|
||||
summary: 'Get a typebot',
|
||||
tags: ['Typebot'],
|
||||
@ -30,7 +31,7 @@ export const getTypebot = authenticatedProcedure
|
||||
.output(
|
||||
z.object({
|
||||
typebot: typebotSchema,
|
||||
isReadOnly: z.boolean(),
|
||||
currentUserMode: z.enum(['guest', 'read', 'write']),
|
||||
})
|
||||
)
|
||||
.query(
|
||||
@ -67,10 +68,7 @@ export const getTypebot = authenticatedProcedure
|
||||
|
||||
return {
|
||||
typebot: parsedTypebot,
|
||||
isReadOnly:
|
||||
existingTypebot.collaborators.find(
|
||||
(collaborator) => collaborator.userId === user.id
|
||||
)?.type === 'READ' ?? false,
|
||||
currentUserMode: getCurrentUserMode(user, existingTypebot),
|
||||
}
|
||||
} catch (err) {
|
||||
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({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/typebots/import',
|
||||
path: '/v1/typebots/import',
|
||||
protect: true,
|
||||
summary: 'Import a typebot',
|
||||
tags: ['Typebot'],
|
||||
|
@ -11,7 +11,7 @@ export const listTypebots = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/typebots',
|
||||
path: '/v1/typebots',
|
||||
protect: true,
|
||||
summary: 'List typebots',
|
||||
tags: ['Typebot'],
|
||||
|
@ -19,7 +19,7 @@ export const publishTypebot = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/typebots/{typebotId}/publish',
|
||||
path: '/v1/typebots/{typebotId}/publish',
|
||||
protect: true,
|
||||
summary: 'Publish a typebot',
|
||||
tags: ['Typebot'],
|
||||
|
@ -8,7 +8,7 @@ export const unpublishTypebot = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/typebots/{typebotId}/unpublish',
|
||||
path: '/v1/typebots/{typebotId}/unpublish',
|
||||
protect: true,
|
||||
summary: 'Unpublish a typebot',
|
||||
tags: ['Typebot'],
|
||||
|
@ -42,7 +42,7 @@ export const updateTypebot = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'PATCH',
|
||||
path: '/typebots/{typebotId}',
|
||||
path: '/v1/typebots/{typebotId}',
|
||||
protect: true,
|
||||
summary: 'Update a typebot',
|
||||
tags: ['Typebot'],
|
||||
|
@ -4,22 +4,34 @@ import {
|
||||
User,
|
||||
Workspace,
|
||||
MemberInWorkspace,
|
||||
Typebot,
|
||||
} from '@typebot.io/prisma'
|
||||
import { settingsSchema } from '@typebot.io/schemas'
|
||||
|
||||
export const isReadTypebotForbidden = async (
|
||||
typebot: {
|
||||
settings?: Typebot['settings']
|
||||
collaborators: Pick<CollaboratorsOnTypebots, 'userId'>[]
|
||||
} & {
|
||||
workspace: Pick<Workspace, 'isSuspended' | 'isPastDue'> & {
|
||||
members: Pick<MemberInWorkspace, 'userId'>[]
|
||||
}
|
||||
},
|
||||
user: Pick<User, 'email' | 'id'>
|
||||
) =>
|
||||
typebot.workspace.isSuspended ||
|
||||
typebot.workspace.isPastDue ||
|
||||
(env.ADMIN_EMAIL !== user.email &&
|
||||
!typebot.collaborators.some(
|
||||
(collaborator) => collaborator.userId === user.id
|
||||
) &&
|
||||
!typebot.workspace.members.some((member) => member.userId === user.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.isPastDue ||
|
||||
(env.ADMIN_EMAIL !== user.email &&
|
||||
!typebot.collaborators.some(
|
||||
(collaborator) => collaborator.userId === user.id
|
||||
) &&
|
||||
!typebot.workspace.members.some((member) => member.userId === user.id))
|
||||
)
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ export const sanitizeSettings = (
|
||||
mode: 'create' | 'update'
|
||||
): Typebot['settings'] => ({
|
||||
...settings,
|
||||
publicShare: mode === 'create' ? undefined : settings.publicShare,
|
||||
general:
|
||||
workspacePlan === Plan.FREE || settings.general
|
||||
? {
|
||||
|
@ -10,7 +10,7 @@ export const createWorkspace = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/workspaces',
|
||||
path: '/v1/workspaces',
|
||||
protect: true,
|
||||
summary: 'Create workspace',
|
||||
tags: ['Workspace'],
|
||||
|
@ -8,7 +8,7 @@ export const deleteWorkspace = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'DELETE',
|
||||
path: '/workspaces/{workspaceId}',
|
||||
path: '/v1/workspaces/{workspaceId}',
|
||||
protect: true,
|
||||
summary: 'Delete workspace',
|
||||
tags: ['Workspace'],
|
||||
|
@ -9,7 +9,7 @@ export const getWorkspace = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/workspaces/{workspaceId}',
|
||||
path: '/v1/workspaces/{workspaceId}',
|
||||
protect: true,
|
||||
summary: 'Get 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