✨ Add webhook blocks API public endpoints
This commit is contained in:
@ -12,6 +12,7 @@ import { ChangePlanForm } from '../ChangePlanForm'
|
|||||||
export const BillingContent = () => {
|
export const BillingContent = () => {
|
||||||
const { workspace, refreshWorkspace } = useWorkspace()
|
const { workspace, refreshWorkspace } = useWorkspace()
|
||||||
|
|
||||||
|
console.log(workspace)
|
||||||
if (!workspace) return null
|
if (!workspace) return null
|
||||||
return (
|
return (
|
||||||
<Stack spacing="10" w="full">
|
<Stack spacing="10" w="full">
|
||||||
@ -20,13 +21,7 @@ export const BillingContent = () => {
|
|||||||
<CurrentSubscriptionContent
|
<CurrentSubscriptionContent
|
||||||
plan={workspace.plan}
|
plan={workspace.plan}
|
||||||
stripeId={workspace.stripeId}
|
stripeId={workspace.stripeId}
|
||||||
onCancelSuccess={() =>
|
onCancelSuccess={refreshWorkspace}
|
||||||
refreshWorkspace({
|
|
||||||
plan: Plan.FREE,
|
|
||||||
additionalChatsIndex: 0,
|
|
||||||
additionalStorageIndex: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<HStack maxW="500px">
|
<HStack maxW="500px">
|
||||||
<StripeClimateLogo />
|
<StripeClimateLogo />
|
||||||
|
@ -52,11 +52,7 @@ export const ChangePlanForm = () => {
|
|||||||
additionalChatsIndex: selectedChatsLimitIndex,
|
additionalChatsIndex: selectedChatsLimitIndex,
|
||||||
additionalStorageIndex: selectedStorageLimitIndex,
|
additionalStorageIndex: selectedStorageLimitIndex,
|
||||||
})
|
})
|
||||||
refreshWorkspace({
|
refreshWorkspace()
|
||||||
plan,
|
|
||||||
additionalChatsIndex: selectedChatsLimitIndex,
|
|
||||||
additionalStorageIndex: selectedStorageLimitIndex,
|
|
||||||
})
|
|
||||||
showToast({
|
showToast({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
description: `Workspace ${plan} plan successfully updated 🎉`,
|
description: `Workspace ${plan} plan successfully updated 🎉`,
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
export * from './router'
|
@ -0,0 +1,74 @@
|
|||||||
|
import { getLinkedTypebots } from '@/features/blocks/logic/typebotLink/api'
|
||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { canReadTypebot } from '@/utils/api/dbRules'
|
||||||
|
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { Typebot, Webhook } from 'models'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { parseResultExample } from '../utils'
|
||||||
|
|
||||||
|
export const getResultExampleProcedure = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'GET',
|
||||||
|
path: '/typebots/{typebotId}/webhookBlocks/{blockId}/getResultExample',
|
||||||
|
protect: true,
|
||||||
|
summary: 'Get result example',
|
||||||
|
description:
|
||||||
|
'Returns "fake" result for webhook block to help you anticipate how the webhook will behave.',
|
||||||
|
tags: ['Webhook'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
typebotId: z.string(),
|
||||||
|
blockId: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
resultExample: z
|
||||||
|
.object({
|
||||||
|
message: z.literal(
|
||||||
|
'This is a sample result, it has been generated ⬇️'
|
||||||
|
),
|
||||||
|
'Submitted at': z.string(),
|
||||||
|
})
|
||||||
|
.and(z.record(z.string().optional()))
|
||||||
|
.describe('Can contain any fields.'),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ input: { typebotId, blockId }, ctx: { user } }) => {
|
||||||
|
const typebot = (await prisma.typebot.findFirst({
|
||||||
|
where: canReadTypebot(typebotId, user),
|
||||||
|
select: {
|
||||||
|
groups: true,
|
||||||
|
edges: true,
|
||||||
|
variables: true,
|
||||||
|
webhooks: true,
|
||||||
|
},
|
||||||
|
})) as
|
||||||
|
| (Pick<Typebot, 'groups' | 'edges' | 'variables'> & {
|
||||||
|
webhooks: Webhook[]
|
||||||
|
})
|
||||||
|
| null
|
||||||
|
|
||||||
|
if (!typebot)
|
||||||
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
|
||||||
|
|
||||||
|
const block = typebot.groups
|
||||||
|
.flatMap((g) => g.blocks)
|
||||||
|
.find((s) => s.id === blockId)
|
||||||
|
|
||||||
|
if (!block)
|
||||||
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Block not found' })
|
||||||
|
|
||||||
|
const linkedTypebots = await getLinkedTypebots(typebot, user)
|
||||||
|
|
||||||
|
return {
|
||||||
|
resultExample: await parseResultExample(
|
||||||
|
typebot,
|
||||||
|
linkedTypebots
|
||||||
|
)(block.groupId),
|
||||||
|
}
|
||||||
|
})
|
@ -0,0 +1,4 @@
|
|||||||
|
export * from './getResultExampleProcedure'
|
||||||
|
export * from './listWebhookBlocksProcedure'
|
||||||
|
export * from './subscribeWebhookProcedure'
|
||||||
|
export * from './unsubscribeWebhookProcedure'
|
@ -0,0 +1,65 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { canReadTypebot } from '@/utils/api/dbRules'
|
||||||
|
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { Group, Typebot, Webhook, WebhookBlock } from 'models'
|
||||||
|
import { byId, isWebhookBlock } from 'utils'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const listWebhookBlocksProcedure = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'GET',
|
||||||
|
path: '/typebots/{typebotId}/webhookBlocks',
|
||||||
|
protect: true,
|
||||||
|
summary: 'List webhook blocks',
|
||||||
|
description:
|
||||||
|
'Returns a list of all the webhook blocks that you can subscribe to.',
|
||||||
|
tags: ['Webhook'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
typebotId: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
webhookBlocks: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
label: z.string(),
|
||||||
|
url: z.string().optional(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ input: { typebotId }, ctx: { user } }) => {
|
||||||
|
const typebot = (await prisma.typebot.findFirst({
|
||||||
|
where: canReadTypebot(typebotId, user),
|
||||||
|
select: {
|
||||||
|
groups: true,
|
||||||
|
webhooks: true,
|
||||||
|
},
|
||||||
|
})) as (Pick<Typebot, 'groups'> & { webhooks: Webhook[] }) | null
|
||||||
|
if (!typebot)
|
||||||
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
|
||||||
|
|
||||||
|
const webhookBlocks = (typebot?.groups as Group[]).reduce<
|
||||||
|
{ id: string; label: string; url: string | undefined }[]
|
||||||
|
>((webhookBlocks, group) => {
|
||||||
|
const blocks = group.blocks.filter((block) =>
|
||||||
|
isWebhookBlock(block)
|
||||||
|
) as WebhookBlock[]
|
||||||
|
return [
|
||||||
|
...webhookBlocks,
|
||||||
|
...blocks.map((b) => ({
|
||||||
|
id: b.id,
|
||||||
|
label: `${group.title} > ${b.id}`,
|
||||||
|
url: typebot?.webhooks.find(byId(b.webhookId))?.url ?? undefined,
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return { webhookBlocks }
|
||||||
|
})
|
@ -0,0 +1,64 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { canWriteTypebot } from '@/utils/api/dbRules'
|
||||||
|
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { Typebot, Webhook, WebhookBlock } from 'models'
|
||||||
|
import { byId, isWebhookBlock } from 'utils'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const subscribeWebhookProcedure = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'POST',
|
||||||
|
path: '/typebots/{typebotId}/webhookBlocks/{blockId}/subscribe',
|
||||||
|
protect: true,
|
||||||
|
summary: 'Subscribe to webhook block',
|
||||||
|
tags: ['Webhook'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
typebotId: z.string(),
|
||||||
|
blockId: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
url: z.string().nullable(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ input: { typebotId, blockId, url }, ctx: { user } }) => {
|
||||||
|
const typebot = (await prisma.typebot.findFirst({
|
||||||
|
where: canWriteTypebot(typebotId, user),
|
||||||
|
select: {
|
||||||
|
groups: true,
|
||||||
|
webhooks: true,
|
||||||
|
},
|
||||||
|
})) as (Pick<Typebot, 'groups'> & { webhooks: Webhook[] }) | null
|
||||||
|
|
||||||
|
if (!typebot)
|
||||||
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
|
||||||
|
|
||||||
|
const webhookBlock = typebot.groups
|
||||||
|
.flatMap((g) => g.blocks)
|
||||||
|
.find(byId(blockId)) as WebhookBlock | null
|
||||||
|
|
||||||
|
if (!webhookBlock || !isWebhookBlock(webhookBlock))
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'Webhook block not found',
|
||||||
|
})
|
||||||
|
|
||||||
|
await prisma.webhook.upsert({
|
||||||
|
where: { id: webhookBlock.webhookId },
|
||||||
|
update: { url, body: '{{state}}', method: 'POST' },
|
||||||
|
create: { url, body: '{{state}}', method: 'POST', typebotId },
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: blockId,
|
||||||
|
url,
|
||||||
|
}
|
||||||
|
})
|
@ -0,0 +1,62 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { canWriteTypebot } from '@/utils/api/dbRules'
|
||||||
|
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { Typebot, Webhook, WebhookBlock } from 'models'
|
||||||
|
import { byId, isWebhookBlock } from 'utils'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const unsubscribeWebhookProcedure = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'POST',
|
||||||
|
path: '/typebots/{typebotId}/webhookBlocks/{blockId}/unsubscribe',
|
||||||
|
protect: true,
|
||||||
|
summary: 'Unsubscribe from webhook block',
|
||||||
|
tags: ['Webhook'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
typebotId: z.string(),
|
||||||
|
blockId: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
url: z.string().nullable(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ input: { typebotId, blockId }, ctx: { user } }) => {
|
||||||
|
const typebot = (await prisma.typebot.findFirst({
|
||||||
|
where: canWriteTypebot(typebotId, user),
|
||||||
|
select: {
|
||||||
|
groups: true,
|
||||||
|
webhooks: true,
|
||||||
|
},
|
||||||
|
})) as (Pick<Typebot, 'groups'> & { webhooks: Webhook[] }) | null
|
||||||
|
|
||||||
|
if (!typebot)
|
||||||
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
|
||||||
|
|
||||||
|
const webhookBlock = typebot.groups
|
||||||
|
.flatMap((g) => g.blocks)
|
||||||
|
.find(byId(blockId)) as WebhookBlock | null
|
||||||
|
|
||||||
|
if (!webhookBlock || !isWebhookBlock(webhookBlock))
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'Webhook block not found',
|
||||||
|
})
|
||||||
|
|
||||||
|
await prisma.webhook.update({
|
||||||
|
where: { id: webhookBlock.webhookId },
|
||||||
|
data: { url: null },
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: blockId,
|
||||||
|
url: null,
|
||||||
|
}
|
||||||
|
})
|
@ -0,0 +1,14 @@
|
|||||||
|
import { router } from '@/utils/server/trpc'
|
||||||
|
import {
|
||||||
|
listWebhookBlocksProcedure,
|
||||||
|
subscribeWebhookProcedure,
|
||||||
|
unsubscribeWebhookProcedure,
|
||||||
|
getResultExampleProcedure,
|
||||||
|
} from './procedures'
|
||||||
|
|
||||||
|
export const webhookRouter = router({
|
||||||
|
listWebhookBlocks: listWebhookBlocksProcedure,
|
||||||
|
getResultExample: getResultExampleProcedure,
|
||||||
|
subscribeWebhook: subscribeWebhookProcedure,
|
||||||
|
unsubscribeWebhook: unsubscribeWebhookProcedure,
|
||||||
|
})
|
@ -0,0 +1 @@
|
|||||||
|
export * from './parseResultExample'
|
@ -0,0 +1,197 @@
|
|||||||
|
import {
|
||||||
|
InputBlock,
|
||||||
|
InputBlockType,
|
||||||
|
LogicBlockType,
|
||||||
|
PublicTypebot,
|
||||||
|
ResultHeaderCell,
|
||||||
|
Block,
|
||||||
|
Typebot,
|
||||||
|
TypebotLinkBlock,
|
||||||
|
} from 'models'
|
||||||
|
import { isInputBlock, byId, parseResultHeader, isNotDefined } from 'utils'
|
||||||
|
|
||||||
|
export const parseResultExample =
|
||||||
|
(
|
||||||
|
typebot: Pick<Typebot | PublicTypebot, 'groups' | 'variables' | 'edges'>,
|
||||||
|
linkedTypebots: (Typebot | PublicTypebot)[]
|
||||||
|
) =>
|
||||||
|
async (
|
||||||
|
currentGroupId: string
|
||||||
|
): Promise<
|
||||||
|
{
|
||||||
|
message: 'This is a sample result, it has been generated ⬇️'
|
||||||
|
'Submitted at': string
|
||||||
|
} & { [k: string]: string | undefined }
|
||||||
|
> => {
|
||||||
|
const header = parseResultHeader(typebot, linkedTypebots)
|
||||||
|
const linkedInputBlocks = await extractLinkedInputBlocks(
|
||||||
|
typebot,
|
||||||
|
linkedTypebots
|
||||||
|
)(currentGroupId)
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'This is a sample result, it has been generated ⬇️',
|
||||||
|
'Submitted at': new Date().toISOString(),
|
||||||
|
...parseResultSample(linkedInputBlocks, header),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractLinkedInputBlocks =
|
||||||
|
(
|
||||||
|
typebot: Pick<Typebot | PublicTypebot, 'groups' | 'variables' | 'edges'>,
|
||||||
|
linkedTypebots: (Typebot | PublicTypebot)[]
|
||||||
|
) =>
|
||||||
|
async (
|
||||||
|
currentGroupId?: string,
|
||||||
|
direction: 'backward' | 'forward' = 'backward'
|
||||||
|
): Promise<InputBlock[]> => {
|
||||||
|
const previousLinkedTypebotBlocks = walkEdgesAndExtract(
|
||||||
|
'linkedBot',
|
||||||
|
direction,
|
||||||
|
typebot
|
||||||
|
)({
|
||||||
|
groupId: currentGroupId,
|
||||||
|
}) as TypebotLinkBlock[]
|
||||||
|
|
||||||
|
const linkedBotInputs =
|
||||||
|
previousLinkedTypebotBlocks.length > 0
|
||||||
|
? await Promise.all(
|
||||||
|
previousLinkedTypebotBlocks.map((linkedBot) =>
|
||||||
|
extractLinkedInputBlocks(
|
||||||
|
linkedTypebots.find((t) =>
|
||||||
|
'typebotId' in t
|
||||||
|
? t.typebotId === linkedBot.options.typebotId
|
||||||
|
: t.id === linkedBot.options.typebotId
|
||||||
|
) as Typebot | PublicTypebot,
|
||||||
|
linkedTypebots
|
||||||
|
)(linkedBot.options.groupId, 'forward')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
|
||||||
|
return (
|
||||||
|
walkEdgesAndExtract(
|
||||||
|
'input',
|
||||||
|
direction,
|
||||||
|
typebot
|
||||||
|
)({
|
||||||
|
groupId: currentGroupId,
|
||||||
|
}) as InputBlock[]
|
||||||
|
).concat(linkedBotInputs.flatMap((l) => l))
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseResultSample = (
|
||||||
|
inputBlocks: InputBlock[],
|
||||||
|
headerCells: ResultHeaderCell[]
|
||||||
|
) =>
|
||||||
|
headerCells.reduce<Record<string, string | undefined>>(
|
||||||
|
(resultSample, cell) => {
|
||||||
|
const inputBlock = inputBlocks.find((inputBlock) =>
|
||||||
|
cell.blocks?.some((block) => block.id === inputBlock.id)
|
||||||
|
)
|
||||||
|
if (isNotDefined(inputBlock)) {
|
||||||
|
if (cell.variableIds)
|
||||||
|
return {
|
||||||
|
...resultSample,
|
||||||
|
[cell.label]: 'content',
|
||||||
|
}
|
||||||
|
return resultSample
|
||||||
|
}
|
||||||
|
const value = getSampleValue(inputBlock)
|
||||||
|
return {
|
||||||
|
...resultSample,
|
||||||
|
[cell.label]: value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
const getSampleValue = (block: InputBlock) => {
|
||||||
|
switch (block.type) {
|
||||||
|
case InputBlockType.CHOICE:
|
||||||
|
return block.options.isMultipleChoice
|
||||||
|
? block.items.map((i) => i.content).join(', ')
|
||||||
|
: block.items[0]?.content ?? 'Item'
|
||||||
|
case InputBlockType.DATE:
|
||||||
|
return new Date().toUTCString()
|
||||||
|
case InputBlockType.EMAIL:
|
||||||
|
return 'test@email.com'
|
||||||
|
case InputBlockType.NUMBER:
|
||||||
|
return '20'
|
||||||
|
case InputBlockType.PHONE:
|
||||||
|
return '+33665566773'
|
||||||
|
case InputBlockType.TEXT:
|
||||||
|
return 'answer value'
|
||||||
|
case InputBlockType.URL:
|
||||||
|
return 'https://test.com'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const walkEdgesAndExtract =
|
||||||
|
(
|
||||||
|
type: 'input' | 'linkedBot',
|
||||||
|
direction: 'backward' | 'forward',
|
||||||
|
typebot: Pick<Typebot | PublicTypebot, 'groups' | 'variables' | 'edges'>
|
||||||
|
) =>
|
||||||
|
({ groupId }: { groupId?: string }): Block[] => {
|
||||||
|
const currentGroupId =
|
||||||
|
groupId ??
|
||||||
|
(typebot.groups.find((b) => b.blocks[0].type === 'start')?.id as string)
|
||||||
|
const blocksInGroup = extractBlocksInGroup(
|
||||||
|
type,
|
||||||
|
typebot
|
||||||
|
)({
|
||||||
|
groupId: currentGroupId,
|
||||||
|
})
|
||||||
|
const otherGroupIds = getGroupIds(typebot, direction)(currentGroupId)
|
||||||
|
return [
|
||||||
|
...blocksInGroup,
|
||||||
|
...otherGroupIds.flatMap((groupId) =>
|
||||||
|
extractBlocksInGroup(type, typebot)({ groupId })
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getGroupIds =
|
||||||
|
(
|
||||||
|
typebot: Pick<Typebot | PublicTypebot, 'groups' | 'variables' | 'edges'>,
|
||||||
|
direction: 'backward' | 'forward',
|
||||||
|
existingGroupIds?: string[]
|
||||||
|
) =>
|
||||||
|
(groupId: string): string[] => {
|
||||||
|
const groups = typebot.edges.reduce<string[]>((groupIds, edge) => {
|
||||||
|
if (direction === 'forward')
|
||||||
|
return (!existingGroupIds ||
|
||||||
|
!existingGroupIds?.includes(edge.to.groupId)) &&
|
||||||
|
edge.from.groupId === groupId
|
||||||
|
? [...groupIds, edge.to.groupId]
|
||||||
|
: groupIds
|
||||||
|
return (!existingGroupIds ||
|
||||||
|
!existingGroupIds.includes(edge.from.groupId)) &&
|
||||||
|
edge.to.groupId === groupId
|
||||||
|
? [...groupIds, edge.from.groupId]
|
||||||
|
: groupIds
|
||||||
|
}, [])
|
||||||
|
const newGroups = [...(existingGroupIds ?? []), ...groups]
|
||||||
|
return groups.concat(
|
||||||
|
groups.flatMap(getGroupIds(typebot, direction, newGroups))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractBlocksInGroup =
|
||||||
|
(
|
||||||
|
type: 'input' | 'linkedBot',
|
||||||
|
typebot: Pick<Typebot | PublicTypebot, 'groups' | 'variables' | 'edges'>
|
||||||
|
) =>
|
||||||
|
({ groupId, blockId }: { groupId: string; blockId?: string }) => {
|
||||||
|
const currentGroup = typebot.groups.find(byId(groupId))
|
||||||
|
if (!currentGroup) return []
|
||||||
|
const blocks: Block[] = []
|
||||||
|
for (const block of currentGroup.blocks) {
|
||||||
|
if (block.id === blockId) break
|
||||||
|
if (type === 'input' && isInputBlock(block)) blocks.push(block)
|
||||||
|
if (type === 'linkedBot' && block.type === LogicBlockType.TYPEBOT_LINK)
|
||||||
|
blocks.push(block)
|
||||||
|
}
|
||||||
|
return blocks
|
||||||
|
}
|
@ -6,8 +6,9 @@ import {
|
|||||||
import { HttpMethod } from 'models'
|
import { HttpMethod } from 'models'
|
||||||
import cuid from 'cuid'
|
import cuid from 'cuid'
|
||||||
import { getTestAsset } from '@/test/utils/playwright'
|
import { getTestAsset } from '@/test/utils/playwright'
|
||||||
|
import { apiToken } from 'utils/playwright/databaseSetup'
|
||||||
|
|
||||||
test.describe('Webhook block', () => {
|
test.describe('Builder', () => {
|
||||||
test('easy configuration should work', async ({ page }) => {
|
test('easy configuration should work', async ({ page }) => {
|
||||||
const typebotId = cuid()
|
const typebotId = cuid()
|
||||||
await importTypebotInDatabase(
|
await importTypebotInDatabase(
|
||||||
@ -98,3 +99,83 @@ const addTestVariable = async (page: Page, name: string, value: string) => {
|
|||||||
await page.click(`text="${name}"`)
|
await page.click(`text="${name}"`)
|
||||||
await page.fill('input >> nth=-1', value)
|
await page.fill('input >> nth=-1', value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test.describe('API', () => {
|
||||||
|
const typebotId = 'webhook-flow'
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
try {
|
||||||
|
await importTypebotInDatabase(getTestAsset('typebots/api.json'), {
|
||||||
|
id: typebotId,
|
||||||
|
})
|
||||||
|
await createWebhook(typebotId)
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('can get webhook blocks', async ({ request }) => {
|
||||||
|
const response = await request.get(
|
||||||
|
`/api/v1/typebots/${typebotId}/webhookBlocks`,
|
||||||
|
{
|
||||||
|
headers: { Authorization: `Bearer ${apiToken}` },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const { webhookBlocks } = await response.json()
|
||||||
|
expect(webhookBlocks).toHaveLength(1)
|
||||||
|
expect(webhookBlocks[0]).toEqual({
|
||||||
|
id: 'webhookBlock',
|
||||||
|
label: 'Webhook > webhookBlock',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('can subscribe webhook', async ({ request }) => {
|
||||||
|
const url = 'https://test.com'
|
||||||
|
const response = await request.post(
|
||||||
|
`/api/v1/typebots/${typebotId}/webhookBlocks/webhookBlock/subscribe`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiToken}`,
|
||||||
|
},
|
||||||
|
data: { url },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const body = await response.json()
|
||||||
|
expect(body).toEqual({
|
||||||
|
id: 'webhookBlock',
|
||||||
|
url,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('can unsubscribe webhook', async ({ request }) => {
|
||||||
|
const response = await request.post(
|
||||||
|
`/api/v1/typebots/${typebotId}/webhookBlocks/webhookBlock/unsubscribe`,
|
||||||
|
{
|
||||||
|
headers: { Authorization: `Bearer ${apiToken}` },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const body = await response.json()
|
||||||
|
expect(body).toEqual({
|
||||||
|
id: 'webhookBlock',
|
||||||
|
url: null,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('can get a sample result', async ({ request }) => {
|
||||||
|
const response = await request.get(
|
||||||
|
`/api/v1/typebots/${typebotId}/webhookBlocks/webhookBlock/getResultExample`,
|
||||||
|
{
|
||||||
|
headers: { Authorization: `Bearer ${apiToken}` },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const data = await response.json()
|
||||||
|
expect(data.resultExample).toMatchObject({
|
||||||
|
message: 'This is a sample result, it has been generated ⬇️',
|
||||||
|
Welcome: 'Hi!',
|
||||||
|
Email: 'test@email.com',
|
||||||
|
Name: 'answer value',
|
||||||
|
Services: 'Website dev, Content Marketing, Social Media, UI / UX Design',
|
||||||
|
'Additional information': 'answer value',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
export * from './utils'
|
@ -0,0 +1,41 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { canReadTypebots } from '@/utils/api/dbRules'
|
||||||
|
import { User } from 'db'
|
||||||
|
import {
|
||||||
|
LogicBlockType,
|
||||||
|
PublicTypebot,
|
||||||
|
Typebot,
|
||||||
|
TypebotLinkBlock,
|
||||||
|
} from 'models'
|
||||||
|
import { isDefined } from 'utils'
|
||||||
|
|
||||||
|
export const getLinkedTypebots = async (
|
||||||
|
typebot: Pick<PublicTypebot, 'groups'>,
|
||||||
|
user?: User
|
||||||
|
): Promise<(Typebot | PublicTypebot)[]> => {
|
||||||
|
const linkedTypebotIds = (
|
||||||
|
typebot.groups
|
||||||
|
.flatMap((g) => g.blocks)
|
||||||
|
.filter(
|
||||||
|
(s) =>
|
||||||
|
s.type === LogicBlockType.TYPEBOT_LINK &&
|
||||||
|
isDefined(s.options.typebotId)
|
||||||
|
) as TypebotLinkBlock[]
|
||||||
|
).map((s) => s.options.typebotId as string)
|
||||||
|
if (linkedTypebotIds.length === 0) return []
|
||||||
|
const typebots = (await ('typebotId' in typebot
|
||||||
|
? prisma.publicTypebot.findMany({
|
||||||
|
where: { id: { in: linkedTypebotIds } },
|
||||||
|
})
|
||||||
|
: prisma.typebot.findMany({
|
||||||
|
where: user
|
||||||
|
? {
|
||||||
|
AND: [
|
||||||
|
{ id: { in: linkedTypebotIds } },
|
||||||
|
canReadTypebots(linkedTypebotIds, user as User),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: { id: { in: linkedTypebotIds } },
|
||||||
|
}))) as unknown as (Typebot | PublicTypebot)[]
|
||||||
|
return typebots
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export * from './getLinkedTypebots'
|
@ -24,7 +24,6 @@ export const TypebotsDropdown = ({
|
|||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
const { typebots, isLoading } = useTypebots({
|
const { typebots, isLoading } = useTypebots({
|
||||||
workspaceId: currentWorkspaceId,
|
workspaceId: currentWorkspaceId,
|
||||||
allFolders: true,
|
|
||||||
onError: (e) => showToast({ title: e.name, description: e.message }),
|
onError: (e) => showToast({ title: e.name, description: e.message }),
|
||||||
})
|
})
|
||||||
const currentTypebot = useMemo(
|
const currentTypebot = useMemo(
|
||||||
|
@ -1,38 +1,18 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import { HStack, Flex, Button, useDisclosure } from '@chakra-ui/react'
|
||||||
Menu,
|
import { SettingsIcon } from '@/components/icons'
|
||||||
MenuButton,
|
|
||||||
MenuList,
|
|
||||||
MenuItem,
|
|
||||||
Text,
|
|
||||||
HStack,
|
|
||||||
Flex,
|
|
||||||
SkeletonCircle,
|
|
||||||
Button,
|
|
||||||
useDisclosure,
|
|
||||||
} from '@chakra-ui/react'
|
|
||||||
import {
|
|
||||||
ChevronLeftIcon,
|
|
||||||
HardDriveIcon,
|
|
||||||
LogOutIcon,
|
|
||||||
PlusIcon,
|
|
||||||
SettingsIcon,
|
|
||||||
} from '@/components/icons'
|
|
||||||
import { signOut } from 'next-auth/react'
|
import { signOut } from 'next-auth/react'
|
||||||
import { useUser } from '@/features/account'
|
import { useUser } from '@/features/account'
|
||||||
import { useWorkspace } from '@/features/workspace'
|
import { useWorkspace, WorkspaceDropdown } from '@/features/workspace'
|
||||||
import { isNotDefined } from 'utils'
|
import { isNotDefined } from 'utils'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { EmojiOrImageIcon } from '@/components/EmojiOrImageIcon'
|
|
||||||
import { TypebotLogo } from '@/components/TypebotLogo'
|
import { TypebotLogo } from '@/components/TypebotLogo'
|
||||||
import { PlanTag } from '@/features/billing'
|
|
||||||
import { WorkspaceSettingsModal } from '@/features/workspace'
|
import { WorkspaceSettingsModal } from '@/features/workspace'
|
||||||
|
|
||||||
export const DashboardHeader = () => {
|
export const DashboardHeader = () => {
|
||||||
const { user } = useUser()
|
const { user } = useUser()
|
||||||
|
const { workspace, switchWorkspace, createWorkspace } = useWorkspace()
|
||||||
|
|
||||||
const { workspace, workspaces, switchWorkspace, createWorkspace } =
|
|
||||||
useWorkspace()
|
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
|
|
||||||
const handleLogOut = () => {
|
const handleLogOut = () => {
|
||||||
@ -71,63 +51,12 @@ export const DashboardHeader = () => {
|
|||||||
>
|
>
|
||||||
Settings & Members
|
Settings & Members
|
||||||
</Button>
|
</Button>
|
||||||
<Menu placement="bottom-end">
|
<WorkspaceDropdown
|
||||||
<MenuButton as={Button} variant="outline" px="2">
|
currentWorkspace={workspace}
|
||||||
<HStack>
|
onLogoutClick={handleLogOut}
|
||||||
<SkeletonCircle
|
onCreateNewWorkspaceClick={handleCreateNewWorkspace}
|
||||||
isLoaded={workspace !== undefined}
|
onWorkspaceSelected={switchWorkspace}
|
||||||
alignItems="center"
|
/>
|
||||||
display="flex"
|
|
||||||
boxSize="20px"
|
|
||||||
>
|
|
||||||
<EmojiOrImageIcon
|
|
||||||
boxSize="20px"
|
|
||||||
icon={workspace?.icon}
|
|
||||||
defaultIcon={HardDriveIcon}
|
|
||||||
/>
|
|
||||||
</SkeletonCircle>
|
|
||||||
{workspace && (
|
|
||||||
<>
|
|
||||||
<Text noOfLines={1} maxW="200px">
|
|
||||||
{workspace.name}
|
|
||||||
</Text>
|
|
||||||
<PlanTag plan={workspace.plan} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<ChevronLeftIcon transform="rotate(-90deg)" />
|
|
||||||
</HStack>
|
|
||||||
</MenuButton>
|
|
||||||
<MenuList>
|
|
||||||
{workspaces
|
|
||||||
?.filter((w) => w.id !== workspace?.id)
|
|
||||||
.map((workspace) => (
|
|
||||||
<MenuItem
|
|
||||||
key={workspace.id}
|
|
||||||
onClick={() => switchWorkspace(workspace.id)}
|
|
||||||
>
|
|
||||||
<HStack>
|
|
||||||
<EmojiOrImageIcon
|
|
||||||
icon={workspace.icon}
|
|
||||||
boxSize="16px"
|
|
||||||
defaultIcon={HardDriveIcon}
|
|
||||||
/>
|
|
||||||
<Text>{workspace.name}</Text>
|
|
||||||
<PlanTag plan={workspace.plan} />
|
|
||||||
</HStack>
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
<MenuItem onClick={handleCreateNewWorkspace} icon={<PlusIcon />}>
|
|
||||||
New workspace
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
onClick={handleLogOut}
|
|
||||||
icon={<LogOutIcon />}
|
|
||||||
color="orange.500"
|
|
||||||
>
|
|
||||||
Log out
|
|
||||||
</MenuItem>
|
|
||||||
</MenuList>
|
|
||||||
</Menu>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -1,31 +1,29 @@
|
|||||||
import { fetcher } from '@/utils/helpers'
|
import { trpc } from '@/lib/trpc'
|
||||||
import { stringify } from 'qs'
|
|
||||||
import useSWR from 'swr'
|
|
||||||
import { env } from 'utils'
|
|
||||||
import { TypebotInDashboard } from '../types'
|
|
||||||
|
|
||||||
export const useTypebots = ({
|
export const useTypebots = ({
|
||||||
folderId,
|
folderId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
allFolders,
|
|
||||||
onError,
|
onError,
|
||||||
}: {
|
}: {
|
||||||
workspaceId?: string
|
workspaceId?: string
|
||||||
folderId?: string
|
folderId?: string | 'root'
|
||||||
allFolders?: boolean
|
|
||||||
onError: (error: Error) => void
|
onError: (error: Error) => void
|
||||||
}) => {
|
}) => {
|
||||||
const params = stringify({ folderId, allFolders, workspaceId })
|
const { data, isLoading, refetch } = trpc.typebot.listTypebots.useQuery(
|
||||||
const { data, error, mutate } = useSWR<
|
{
|
||||||
{ typebots: TypebotInDashboard[] },
|
workspaceId: workspaceId!,
|
||||||
Error
|
folderId,
|
||||||
>(workspaceId ? `/api/typebots?${params}` : null, fetcher, {
|
},
|
||||||
dedupingInterval: env('E2E_TEST') === 'true' ? 0 : undefined,
|
{
|
||||||
})
|
enabled: !!workspaceId,
|
||||||
if (error) onError(error)
|
onError: (error) => {
|
||||||
|
onError(new Error(error.message))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
typebots: data?.typebots,
|
typebots: data?.typebots,
|
||||||
isLoading: !error && !data,
|
isLoading,
|
||||||
mutate,
|
refetch,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,10 +67,10 @@ export const FolderContent = ({ folder }: Props) => {
|
|||||||
const {
|
const {
|
||||||
typebots,
|
typebots,
|
||||||
isLoading: isTypebotLoading,
|
isLoading: isTypebotLoading,
|
||||||
mutate: mutateTypebots,
|
refetch: refetchTypebots,
|
||||||
} = useTypebots({
|
} = useTypebots({
|
||||||
workspaceId: workspace?.id,
|
workspaceId: workspace?.id,
|
||||||
folderId: folder?.id,
|
folderId: folder === null ? 'root' : folder.id,
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
showToast({
|
showToast({
|
||||||
title: "Couldn't fetch typebots",
|
title: "Couldn't fetch typebots",
|
||||||
@ -85,7 +85,7 @@ export const FolderContent = ({ folder }: Props) => {
|
|||||||
folderId: folderId === 'root' ? null : folderId,
|
folderId: folderId === 'root' ? null : folderId,
|
||||||
})
|
})
|
||||||
if (error) showToast({ description: error.message })
|
if (error) showToast({ description: error.message })
|
||||||
mutateTypebots({ typebots: typebots.filter((t) => t.id !== typebotId) })
|
refetchTypebots()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCreateFolder = async () => {
|
const handleCreateFolder = async () => {
|
||||||
@ -105,7 +105,7 @@ export const FolderContent = ({ folder }: Props) => {
|
|||||||
|
|
||||||
const handleTypebotDeleted = (deletedId: string) => {
|
const handleTypebotDeleted = (deletedId: string) => {
|
||||||
if (!typebots) return
|
if (!typebots) return
|
||||||
mutateTypebots({ typebots: typebots.filter((t) => t.id !== deletedId) })
|
refetchTypebots()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFolderDeleted = (deletedId: string) => {
|
const handleFolderDeleted = (deletedId: string) => {
|
||||||
|
@ -17,12 +17,17 @@ export const deleteResultsProcedure = authenticatedProcedure
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
typebotId: z.string(),
|
typebotId: z.string(),
|
||||||
ids: z.string().optional(),
|
resultIds: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
'Comma separated list of ids. If not provided, all results will be deleted. ⚠️'
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.output(z.void())
|
.output(z.void())
|
||||||
.mutation(async ({ input, ctx: { user } }) => {
|
.mutation(async ({ input, ctx: { user } }) => {
|
||||||
const idsArray = input.ids?.split(',')
|
const idsArray = input.resultIds?.split(',')
|
||||||
const { typebotId } = input
|
const { typebotId } = input
|
||||||
const { success } = await archiveResults({
|
const { success } = await archiveResults({
|
||||||
typebotId,
|
typebotId,
|
||||||
|
@ -90,7 +90,7 @@ export const ResultsActionButtons = ({
|
|||||||
if (!workspaceId || !typebotId) return
|
if (!workspaceId || !typebotId) return
|
||||||
deleteResultsMutation.mutate({
|
deleteResultsMutation.mutate({
|
||||||
typebotId,
|
typebotId,
|
||||||
ids:
|
resultIds:
|
||||||
totalSelected === totalResults
|
totalSelected === totalResults
|
||||||
? undefined
|
? undefined
|
||||||
: selectedResultsId.join(','),
|
: selectedResultsId.join(','),
|
||||||
|
1
apps/builder/src/features/typebot/api/index.ts
Normal file
1
apps/builder/src/features/typebot/api/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './router'
|
@ -0,0 +1 @@
|
|||||||
|
export * from './listTypebotsProcedure'
|
@ -0,0 +1,68 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { WorkspaceRole } from 'db'
|
||||||
|
import { Typebot, typebotSchema } from 'models'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const listTypebotsProcedure = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'GET',
|
||||||
|
path: '/typebots',
|
||||||
|
protect: true,
|
||||||
|
summary: 'List typebots',
|
||||||
|
tags: ['Typebot'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(z.object({ workspaceId: z.string(), folderId: z.string().optional() }))
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
typebots: z.array(
|
||||||
|
typebotSchema.pick({
|
||||||
|
name: true,
|
||||||
|
icon: true,
|
||||||
|
id: true,
|
||||||
|
publishedTypebotId: true,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ input: { workspaceId, folderId }, ctx: { user } }) => {
|
||||||
|
const typebots = (await prisma.typebot.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
isArchived: { not: true },
|
||||||
|
folderId: folderId === 'root' ? null : folderId,
|
||||||
|
workspace: {
|
||||||
|
id: workspaceId,
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId: user.id,
|
||||||
|
role: { not: WorkspaceRole.GUEST },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isArchived: { not: true },
|
||||||
|
workspace: {
|
||||||
|
id: workspaceId,
|
||||||
|
members: {
|
||||||
|
some: { userId: user.id, role: WorkspaceRole.GUEST },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
collaborators: { some: { userId: user.id } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
select: { name: true, publishedTypebotId: true, id: true, icon: true },
|
||||||
|
})) as Pick<Typebot, 'name' | 'id' | 'icon' | 'publishedTypebotId'>[]
|
||||||
|
|
||||||
|
if (!typebots)
|
||||||
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'No typebots found' })
|
||||||
|
|
||||||
|
return { typebots }
|
||||||
|
})
|
6
apps/builder/src/features/typebot/api/router.ts
Normal file
6
apps/builder/src/features/typebot/api/router.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { router } from '@/utils/server/trpc'
|
||||||
|
import { listTypebotsProcedure } from './procedures'
|
||||||
|
|
||||||
|
export const typebotRouter = router({
|
||||||
|
listTypebots: listTypebotsProcedure,
|
||||||
|
})
|
@ -1,186 +1,170 @@
|
|||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { byId } from 'utils'
|
import { byId } from 'utils'
|
||||||
import { Plan, Workspace, WorkspaceRole } from 'db'
|
import { WorkspaceRole } from 'db'
|
||||||
import { useUser } from '../account/UserProvider'
|
import { useUser } from '../account/UserProvider'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useTypebot } from '../editor/providers/TypebotProvider'
|
import { trpc } from '@/lib/trpc'
|
||||||
import { useWorkspaces } from './hooks/useWorkspaces'
|
import { Workspace } from 'models'
|
||||||
import { createWorkspaceQuery } from './queries/createWorkspaceQuery'
|
import { useToast } from '@/hooks/useToast'
|
||||||
import { deleteWorkspaceQuery } from './queries/deleteWorkspaceQuery'
|
import { parseNewName, setWorkspaceIdInLocalStorage } from './utils'
|
||||||
import { updateWorkspaceQuery } from './queries/updateWorkspaceQuery'
|
import { useTypebot } from '../editor'
|
||||||
import { WorkspaceWithMembers } from './types'
|
|
||||||
|
|
||||||
const workspaceContext = createContext<{
|
const workspaceContext = createContext<{
|
||||||
workspaces?: WorkspaceWithMembers[]
|
workspaces: Pick<Workspace, 'id' | 'name' | 'icon' | 'plan'>[]
|
||||||
isLoading: boolean
|
workspace?: Workspace
|
||||||
workspace?: WorkspaceWithMembers
|
|
||||||
canEdit: boolean
|
|
||||||
currentRole?: WorkspaceRole
|
currentRole?: WorkspaceRole
|
||||||
switchWorkspace: (workspaceId: string) => void
|
switchWorkspace: (workspaceId: string) => void
|
||||||
createWorkspace: (name?: string) => Promise<void>
|
createWorkspace: (name?: string) => Promise<void>
|
||||||
updateWorkspace: (
|
updateWorkspace: (updates: { icon?: string; name?: string }) => void
|
||||||
workspaceId: string,
|
|
||||||
updates: Partial<Workspace>
|
|
||||||
) => Promise<void>
|
|
||||||
deleteCurrentWorkspace: () => Promise<void>
|
deleteCurrentWorkspace: () => Promise<void>
|
||||||
refreshWorkspace: (expectedUpdates: Partial<Workspace>) => void
|
refreshWorkspace: () => void
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
type WorkspaceContextProps = {
|
type WorkspaceContextProps = {
|
||||||
|
typebotId?: string
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const getNewWorkspaceName = (
|
export const WorkspaceProvider = ({
|
||||||
userFullName: string | undefined,
|
typebotId,
|
||||||
existingWorkspaces: Workspace[]
|
children,
|
||||||
) => {
|
}: WorkspaceContextProps) => {
|
||||||
const workspaceName = userFullName
|
|
||||||
? `${userFullName}'s workspace`
|
|
||||||
: 'My workspace'
|
|
||||||
let newName = workspaceName
|
|
||||||
let i = 1
|
|
||||||
while (existingWorkspaces.find((w) => w.name === newName)) {
|
|
||||||
newName = `${workspaceName} (${i})`
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
return newName
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WorkspaceProvider = ({ children }: WorkspaceContextProps) => {
|
|
||||||
const { query } = useRouter()
|
const { query } = useRouter()
|
||||||
const { user } = useUser()
|
const { user } = useUser()
|
||||||
const userId = user?.id
|
const userId = user?.id
|
||||||
|
const [workspaceId, setWorkspaceId] = useState<string | undefined>()
|
||||||
|
|
||||||
const { typebot } = useTypebot()
|
const { typebot } = useTypebot()
|
||||||
const { workspaces, isLoading, mutate } = useWorkspaces({ userId })
|
|
||||||
const [currentWorkspace, setCurrentWorkspace] =
|
|
||||||
useState<WorkspaceWithMembers>()
|
|
||||||
const [pendingWorkspaceId, setPendingWorkspaceId] = useState<string>()
|
|
||||||
|
|
||||||
const canEdit =
|
const trpcContext = trpc.useContext()
|
||||||
workspaces
|
|
||||||
?.find(byId(currentWorkspace?.id))
|
|
||||||
?.members.find((m) => m.userId === userId)?.role === WorkspaceRole.ADMIN
|
|
||||||
|
|
||||||
const currentRole = currentWorkspace?.members.find(
|
const { data: workspacesData } = trpc.workspace.listWorkspaces.useQuery(
|
||||||
(m) => m.userId === userId
|
undefined,
|
||||||
|
{
|
||||||
|
enabled: !!user,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const workspaces = useMemo(
|
||||||
|
() => workspacesData?.workspaces ?? [],
|
||||||
|
[workspacesData?.workspaces]
|
||||||
|
)
|
||||||
|
|
||||||
|
const { data: workspaceData } = trpc.workspace.getWorkspace.useQuery(
|
||||||
|
{ workspaceId: workspaceId! },
|
||||||
|
{ enabled: !!workspaceId }
|
||||||
|
)
|
||||||
|
|
||||||
|
const { data: membersData } = trpc.workspace.listMembersInWorkspace.useQuery(
|
||||||
|
{ workspaceId: workspaceId! },
|
||||||
|
{ enabled: !!workspaceId }
|
||||||
|
)
|
||||||
|
|
||||||
|
const workspace = workspaceData?.workspace
|
||||||
|
const members = membersData?.members
|
||||||
|
|
||||||
|
const { showToast } = useToast()
|
||||||
|
|
||||||
|
const createWorkspaceMutation = trpc.workspace.createWorkspace.useMutation({
|
||||||
|
onError: (error) => showToast({ description: error.message }),
|
||||||
|
onSuccess: async () => {
|
||||||
|
trpcContext.workspace.listWorkspaces.invalidate()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateWorkspaceMutation = trpc.workspace.updateWorkspace.useMutation({
|
||||||
|
onError: (error) => showToast({ description: error.message }),
|
||||||
|
onSuccess: async () => {
|
||||||
|
trpcContext.workspace.getWorkspace.invalidate()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteWorkspaceMutation = trpc.workspace.deleteWorkspace.useMutation({
|
||||||
|
onError: (error) => showToast({ description: error.message }),
|
||||||
|
onSuccess: async () => {
|
||||||
|
trpcContext.workspace.listWorkspaces.invalidate()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentRole = members?.find(
|
||||||
|
(member) =>
|
||||||
|
member.user.email === user?.email && member.workspaceId === workspaceId
|
||||||
)?.role
|
)?.role
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workspaces || workspaces.length === 0 || currentWorkspace) return
|
if (
|
||||||
|
!workspaces ||
|
||||||
|
workspaces.length === 0 ||
|
||||||
|
workspaceId ||
|
||||||
|
(typebotId && !typebot?.workspaceId)
|
||||||
|
)
|
||||||
|
return
|
||||||
const lastWorspaceId =
|
const lastWorspaceId =
|
||||||
pendingWorkspaceId ??
|
typebot?.workspaceId ??
|
||||||
query.workspaceId?.toString() ??
|
query.workspaceId?.toString() ??
|
||||||
localStorage.getItem('workspaceId')
|
localStorage.getItem('workspaceId')
|
||||||
const defaultWorkspace = lastWorspaceId
|
|
||||||
? workspaces.find(byId(lastWorspaceId))
|
|
||||||
: workspaces.find((w) =>
|
|
||||||
w.members.some(
|
|
||||||
(m) => m.userId === userId && m.role === WorkspaceRole.ADMIN
|
|
||||||
)
|
|
||||||
)
|
|
||||||
setCurrentWorkspace(defaultWorkspace ?? workspaces[0])
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [workspaces?.length])
|
|
||||||
|
|
||||||
useEffect(() => {
|
const defaultWorkspaceId = lastWorspaceId
|
||||||
if (!currentWorkspace?.id) return
|
? workspaces.find(byId(lastWorspaceId))?.id
|
||||||
localStorage.setItem('workspaceId', currentWorkspace.id)
|
: members?.find((member) => member.role === WorkspaceRole.ADMIN)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
?.workspaceId
|
||||||
}, [currentWorkspace?.id])
|
|
||||||
|
|
||||||
useEffect(() => {
|
const newWorkspaceId = defaultWorkspaceId ?? workspaces[0].id
|
||||||
if (!currentWorkspace) return setPendingWorkspaceId(typebot?.workspaceId)
|
setWorkspaceIdInLocalStorage(newWorkspaceId)
|
||||||
if (!typebot?.workspaceId || typebot.workspaceId === currentWorkspace.id)
|
setWorkspaceId(newWorkspaceId)
|
||||||
return
|
}, [
|
||||||
switchWorkspace(typebot.workspaceId)
|
members,
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
query.workspaceId,
|
||||||
}, [typebot?.workspaceId])
|
typebot?.workspaceId,
|
||||||
|
typebotId,
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
workspaces,
|
||||||
|
])
|
||||||
|
|
||||||
const switchWorkspace = (workspaceId: string) => {
|
const switchWorkspace = (workspaceId: string) => {
|
||||||
const newWorkspace = workspaces?.find(byId(workspaceId))
|
setWorkspaceId(workspaceId)
|
||||||
if (!newWorkspace) return
|
setWorkspaceIdInLocalStorage(workspaceId)
|
||||||
setCurrentWorkspace(newWorkspace)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createWorkspace = async (userFullName?: string) => {
|
const createWorkspace = async (userFullName?: string) => {
|
||||||
if (!workspaces) return
|
if (!workspaces) return
|
||||||
const newWorkspaceName = getNewWorkspaceName(userFullName, workspaces)
|
const name = parseNewName(userFullName, workspaces)
|
||||||
const { data, error } = await createWorkspaceQuery({
|
const { workspace } = await createWorkspaceMutation.mutateAsync({ name })
|
||||||
name: newWorkspaceName,
|
setWorkspaceId(workspace.id)
|
||||||
plan: Plan.FREE,
|
|
||||||
})
|
|
||||||
if (error || !data) return
|
|
||||||
const { workspace } = data
|
|
||||||
const newWorkspace = {
|
|
||||||
...workspace,
|
|
||||||
members: [
|
|
||||||
{
|
|
||||||
role: WorkspaceRole.ADMIN,
|
|
||||||
userId: userId as string,
|
|
||||||
workspaceId: workspace.id as string,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
mutate({
|
|
||||||
workspaces: [...workspaces, newWorkspace],
|
|
||||||
})
|
|
||||||
setCurrentWorkspace(newWorkspace)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateWorkspace = async (
|
const updateWorkspace = (updates: { icon?: string; name?: string }) => {
|
||||||
workspaceId: string,
|
if (!workspaceId) return
|
||||||
updates: Partial<Workspace>
|
updateWorkspaceMutation.mutate({
|
||||||
) => {
|
workspaceId,
|
||||||
const { data } = await updateWorkspaceQuery({ id: workspaceId, ...updates })
|
...updates,
|
||||||
if (!data || !currentWorkspace) return
|
|
||||||
setCurrentWorkspace({ ...currentWorkspace, ...updates })
|
|
||||||
mutate({
|
|
||||||
workspaces: (workspaces ?? []).map((w) =>
|
|
||||||
w.id === workspaceId ? { ...data.workspace, members: w.members } : w
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteCurrentWorkspace = async () => {
|
const deleteCurrentWorkspace = async () => {
|
||||||
if (!currentWorkspace || !workspaces || workspaces.length < 2) return
|
if (!workspaceId || !workspaces || workspaces.length < 2) return
|
||||||
const { data } = await deleteWorkspaceQuery(currentWorkspace.id)
|
await deleteWorkspaceMutation.mutateAsync({ workspaceId })
|
||||||
if (!data || !currentWorkspace) return
|
setWorkspaceId(workspaces[0].id)
|
||||||
const newWorkspaces = (workspaces ?? []).filter((w) =>
|
|
||||||
w.id === currentWorkspace.id
|
|
||||||
? { ...data.workspace, members: w.members }
|
|
||||||
: w
|
|
||||||
)
|
|
||||||
setCurrentWorkspace(newWorkspaces[0])
|
|
||||||
mutate({
|
|
||||||
workspaces: newWorkspaces,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshWorkspace = (expectedUpdates: Partial<Workspace>) => {
|
const refreshWorkspace = () => {
|
||||||
if (!currentWorkspace) return
|
trpcContext.workspace.getWorkspace.invalidate()
|
||||||
const updatedWorkspace = { ...currentWorkspace, ...expectedUpdates }
|
|
||||||
mutate({
|
|
||||||
workspaces: (workspaces ?? []).map((w) =>
|
|
||||||
w.id === currentWorkspace.id ? updatedWorkspace : w
|
|
||||||
),
|
|
||||||
})
|
|
||||||
setCurrentWorkspace(updatedWorkspace)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<workspaceContext.Provider
|
<workspaceContext.Provider
|
||||||
value={{
|
value={{
|
||||||
workspaces,
|
workspaces,
|
||||||
workspace: currentWorkspace,
|
workspace,
|
||||||
isLoading,
|
|
||||||
canEdit,
|
|
||||||
currentRole,
|
currentRole,
|
||||||
switchWorkspace,
|
switchWorkspace,
|
||||||
createWorkspace,
|
createWorkspace,
|
||||||
|
1
apps/builder/src/features/workspace/api/index.ts
Normal file
1
apps/builder/src/features/workspace/api/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './router'
|
@ -0,0 +1,56 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { Plan } from 'db'
|
||||||
|
import { Workspace, workspaceSchema } from 'models'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const createWorkspaceProcedure = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'POST',
|
||||||
|
path: '/workspaces',
|
||||||
|
protect: true,
|
||||||
|
summary: 'Create workspace',
|
||||||
|
tags: ['Workspace'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(workspaceSchema.pick({ name: true }))
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
workspace: workspaceSchema,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(async ({ input: { name }, ctx: { user } }) => {
|
||||||
|
const existingWorkspaceNames = (await prisma.workspace.findMany({
|
||||||
|
where: {
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: { name: true },
|
||||||
|
})) satisfies Pick<Workspace, 'name'>[]
|
||||||
|
|
||||||
|
if (existingWorkspaceNames.some((workspace) => workspace.name === name))
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'Workspace with same name already exists',
|
||||||
|
})
|
||||||
|
|
||||||
|
const plan =
|
||||||
|
process.env.ADMIN_EMAIL === user.email ? Plan.LIFETIME : Plan.FREE
|
||||||
|
|
||||||
|
const newWorkspace = (await prisma.workspace.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
members: { create: [{ role: 'ADMIN', userId: user.id }] },
|
||||||
|
plan,
|
||||||
|
},
|
||||||
|
})) satisfies Workspace
|
||||||
|
|
||||||
|
return {
|
||||||
|
workspace: newWorkspace,
|
||||||
|
}
|
||||||
|
})
|
@ -0,0 +1,33 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const deleteWorkspaceProcedure = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'DELETE',
|
||||||
|
path: '/workspaces/{workspaceId}',
|
||||||
|
protect: true,
|
||||||
|
summary: 'Delete workspace',
|
||||||
|
tags: ['Workspace'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
workspaceId: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
message: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(async ({ input: { workspaceId }, ctx: { user } }) => {
|
||||||
|
await prisma.workspace.deleteMany({
|
||||||
|
where: { members: { some: { userId: user.id } }, id: workspaceId },
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Workspace deleted',
|
||||||
|
}
|
||||||
|
})
|
@ -0,0 +1,38 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { Workspace, workspaceSchema } from 'models'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const getWorkspaceProcedure = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'GET',
|
||||||
|
path: '/workspaces/{workspaceId}',
|
||||||
|
protect: true,
|
||||||
|
summary: 'Get workspace',
|
||||||
|
tags: ['Workspace'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
workspaceId: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
workspace: workspaceSchema,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
|
||||||
|
const workspace = (await prisma.workspace.findFirst({
|
||||||
|
where: { members: { some: { userId: user.id } }, id: workspaceId },
|
||||||
|
})) satisfies Workspace | null
|
||||||
|
|
||||||
|
if (!workspace)
|
||||||
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'No workspaces found' })
|
||||||
|
|
||||||
|
return {
|
||||||
|
workspace,
|
||||||
|
}
|
||||||
|
})
|
@ -0,0 +1,7 @@
|
|||||||
|
export * from './createWorkspaceProcedure'
|
||||||
|
export * from './deleteWorkspaceProcedure'
|
||||||
|
export * from './getWorkspaceProcedure'
|
||||||
|
export * from './listInvitationsInWorkspaceProcedure'
|
||||||
|
export * from './listMembersInWorkspaceProcedure'
|
||||||
|
export * from './listWorkspacesProcedure'
|
||||||
|
export * from './updateWorkspaceProcedure'
|
@ -0,0 +1,43 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { WorkspaceInvitation, workspaceInvitationSchema } from 'models'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const listInvitationsInWorkspaceProcedure = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'GET',
|
||||||
|
path: '/workspaces/{workspaceId}/invitations',
|
||||||
|
protect: true,
|
||||||
|
summary: 'List invitations in workspace',
|
||||||
|
tags: ['Workspace'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
workspaceId: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
invitations: z.array(workspaceInvitationSchema),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
|
||||||
|
const invitations = (await prisma.workspaceInvitation.findMany({
|
||||||
|
where: {
|
||||||
|
workspaceId,
|
||||||
|
workspace: { members: { some: { userId: user.id } } },
|
||||||
|
},
|
||||||
|
select: { createdAt: true, email: true, type: true },
|
||||||
|
})) satisfies WorkspaceInvitation[]
|
||||||
|
|
||||||
|
if (!invitations)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'No invitations found',
|
||||||
|
})
|
||||||
|
|
||||||
|
return { invitations }
|
||||||
|
})
|
@ -0,0 +1,37 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { WorkspaceMember, workspaceMemberSchema } from 'models'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const listMembersInWorkspaceProcedure = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'GET',
|
||||||
|
path: '/workspaces/{workspaceId}/members',
|
||||||
|
protect: true,
|
||||||
|
summary: 'List members in workspace',
|
||||||
|
tags: ['Workspace'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
workspaceId: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
members: z.array(workspaceMemberSchema),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
|
||||||
|
const members = (await prisma.memberInWorkspace.findMany({
|
||||||
|
where: { userId: user.id, workspaceId },
|
||||||
|
include: { user: { select: { name: true, email: true, image: true } } },
|
||||||
|
})) satisfies WorkspaceMember[]
|
||||||
|
|
||||||
|
if (!members)
|
||||||
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'No members found' })
|
||||||
|
|
||||||
|
return { members }
|
||||||
|
})
|
@ -0,0 +1,35 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { Workspace, workspaceSchema } from 'models'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const listWorkspacesProcedure = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'GET',
|
||||||
|
path: '/workspaces',
|
||||||
|
protect: true,
|
||||||
|
summary: 'List workspaces',
|
||||||
|
tags: ['Workspace'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(z.void())
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
workspaces: z.array(
|
||||||
|
workspaceSchema.pick({ id: true, name: true, icon: true, plan: true })
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ ctx: { user } }) => {
|
||||||
|
const workspaces = (await prisma.workspace.findMany({
|
||||||
|
where: { members: { some: { userId: user.id } } },
|
||||||
|
select: { name: true, id: true, icon: true, plan: true },
|
||||||
|
})) satisfies Pick<Workspace, 'id' | 'name' | 'icon' | 'plan'>[]
|
||||||
|
|
||||||
|
if (!workspaces)
|
||||||
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'No workspaces found' })
|
||||||
|
|
||||||
|
return { workspaces }
|
||||||
|
})
|
@ -0,0 +1,45 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { Workspace, workspaceSchema } from 'models'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const updateWorkspaceProcedure = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'PATCH',
|
||||||
|
path: '/workspaces/{workspaceId}',
|
||||||
|
protect: true,
|
||||||
|
summary: 'Update workspace',
|
||||||
|
tags: ['Workspace'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
name: z.string().optional(),
|
||||||
|
icon: z.string().optional(),
|
||||||
|
workspaceId: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
workspace: workspaceSchema,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(async ({ input: { workspaceId, ...updates }, ctx: { user } }) => {
|
||||||
|
await prisma.workspace.updateMany({
|
||||||
|
where: { members: { some: { userId: user.id } }, id: workspaceId },
|
||||||
|
data: updates,
|
||||||
|
})
|
||||||
|
|
||||||
|
const workspace = (await prisma.workspace.findFirst({
|
||||||
|
where: { members: { some: { userId: user.id } }, id: workspaceId },
|
||||||
|
})) satisfies Workspace | null
|
||||||
|
|
||||||
|
if (!workspace)
|
||||||
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })
|
||||||
|
|
||||||
|
return {
|
||||||
|
workspace,
|
||||||
|
}
|
||||||
|
})
|
18
apps/builder/src/features/workspace/api/router.ts
Normal file
18
apps/builder/src/features/workspace/api/router.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { router } from '@/utils/server/trpc'
|
||||||
|
import {
|
||||||
|
createWorkspaceProcedure,
|
||||||
|
deleteWorkspaceProcedure,
|
||||||
|
getWorkspaceProcedure,
|
||||||
|
listMembersInWorkspaceProcedure,
|
||||||
|
listWorkspacesProcedure,
|
||||||
|
updateWorkspaceProcedure,
|
||||||
|
} from './procedures'
|
||||||
|
|
||||||
|
export const workspaceRouter = router({
|
||||||
|
listWorkspaces: listWorkspacesProcedure,
|
||||||
|
getWorkspace: getWorkspaceProcedure,
|
||||||
|
listMembersInWorkspace: listMembersInWorkspaceProcedure,
|
||||||
|
createWorkspace: createWorkspaceProcedure,
|
||||||
|
updateWorkspace: updateWorkspaceProcedure,
|
||||||
|
deleteWorkspace: deleteWorkspaceProcedure,
|
||||||
|
})
|
@ -23,11 +23,13 @@ import { Member } from '../../types'
|
|||||||
|
|
||||||
export const MembersList = () => {
|
export const MembersList = () => {
|
||||||
const { user } = useUser()
|
const { user } = useUser()
|
||||||
const { workspace, canEdit } = useWorkspace()
|
const { workspace, currentRole } = useWorkspace()
|
||||||
const { members, invitations, isLoading, mutate } = useMembers({
|
const { members, invitations, isLoading, mutate } = useMembers({
|
||||||
workspaceId: workspace?.id,
|
workspaceId: workspace?.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const canEdit = currentRole === WorkspaceRole.ADMIN
|
||||||
|
|
||||||
const handleDeleteMemberClick = (memberId: string) => async () => {
|
const handleDeleteMemberClick = (memberId: string) => async () => {
|
||||||
if (!workspace) return
|
if (!workspace) return
|
||||||
await deleteMemberQuery(workspace.id, memberId)
|
await deleteMemberQuery(workspace.id, memberId)
|
||||||
|
@ -0,0 +1,98 @@
|
|||||||
|
import { EmojiOrImageIcon } from '@/components/EmojiOrImageIcon'
|
||||||
|
import {
|
||||||
|
HardDriveIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
PlusIcon,
|
||||||
|
LogOutIcon,
|
||||||
|
} from '@/components/icons'
|
||||||
|
import { PlanTag } from '@/features/billing'
|
||||||
|
import { trpc } from '@/lib/trpc'
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
Button,
|
||||||
|
HStack,
|
||||||
|
SkeletonCircle,
|
||||||
|
MenuList,
|
||||||
|
MenuItem,
|
||||||
|
Text,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { Workspace } from 'models'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
currentWorkspace?: Workspace
|
||||||
|
onWorkspaceSelected: (workspaceId: string) => void
|
||||||
|
onCreateNewWorkspaceClick: () => void
|
||||||
|
onLogoutClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WorkspaceDropdown = ({
|
||||||
|
currentWorkspace,
|
||||||
|
onWorkspaceSelected,
|
||||||
|
onLogoutClick,
|
||||||
|
onCreateNewWorkspaceClick,
|
||||||
|
}: Props) => {
|
||||||
|
const { data } = trpc.workspace.listWorkspaces.useQuery()
|
||||||
|
|
||||||
|
const workspaces = data?.workspaces ?? []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu placement="bottom-end">
|
||||||
|
<MenuButton as={Button} variant="outline" px="2">
|
||||||
|
<HStack>
|
||||||
|
<SkeletonCircle
|
||||||
|
isLoaded={currentWorkspace !== undefined}
|
||||||
|
alignItems="center"
|
||||||
|
display="flex"
|
||||||
|
boxSize="20px"
|
||||||
|
>
|
||||||
|
<EmojiOrImageIcon
|
||||||
|
boxSize="20px"
|
||||||
|
icon={currentWorkspace?.icon}
|
||||||
|
defaultIcon={HardDriveIcon}
|
||||||
|
/>
|
||||||
|
</SkeletonCircle>
|
||||||
|
{currentWorkspace && (
|
||||||
|
<>
|
||||||
|
<Text noOfLines={1} maxW="200px">
|
||||||
|
{currentWorkspace.name}
|
||||||
|
</Text>
|
||||||
|
<PlanTag plan={currentWorkspace.plan} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<ChevronLeftIcon transform="rotate(-90deg)" />
|
||||||
|
</HStack>
|
||||||
|
</MenuButton>
|
||||||
|
<MenuList>
|
||||||
|
{workspaces
|
||||||
|
?.filter((workspace) => workspace.id !== currentWorkspace?.id)
|
||||||
|
.map((workspace) => (
|
||||||
|
<MenuItem
|
||||||
|
key={workspace.id}
|
||||||
|
onClick={() => onWorkspaceSelected(workspace.id)}
|
||||||
|
>
|
||||||
|
<HStack>
|
||||||
|
<EmojiOrImageIcon
|
||||||
|
icon={workspace.icon}
|
||||||
|
boxSize="16px"
|
||||||
|
defaultIcon={HardDriveIcon}
|
||||||
|
/>
|
||||||
|
<Text>{workspace.name}</Text>
|
||||||
|
<PlanTag plan={workspace.plan} />
|
||||||
|
</HStack>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
<MenuItem onClick={onCreateNewWorkspaceClick} icon={<PlusIcon />}>
|
||||||
|
New workspace
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={onLogoutClick}
|
||||||
|
icon={<LogOutIcon />}
|
||||||
|
color="orange.500"
|
||||||
|
>
|
||||||
|
Log out
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
)
|
||||||
|
}
|
@ -19,12 +19,11 @@ export const WorkspaceSettingsForm = ({ onClose }: { onClose: () => void }) => {
|
|||||||
|
|
||||||
const handleNameChange = (name: string) => {
|
const handleNameChange = (name: string) => {
|
||||||
if (!workspace?.id) return
|
if (!workspace?.id) return
|
||||||
updateWorkspace(workspace?.id, { name })
|
updateWorkspace({ name })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChangeIcon = (icon: string) => {
|
const handleChangeIcon = (icon: string) => {
|
||||||
if (!workspace?.id) return
|
updateWorkspace({ icon })
|
||||||
updateWorkspace(workspace?.id, { icon })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteClick = async () => {
|
const handleDeleteClick = async () => {
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
UsersIcon,
|
UsersIcon,
|
||||||
} from '@/components/icons'
|
} from '@/components/icons'
|
||||||
import { EmojiOrImageIcon } from '@/components/EmojiOrImageIcon'
|
import { EmojiOrImageIcon } from '@/components/EmojiOrImageIcon'
|
||||||
import { GraphNavigation, User, Workspace } from 'db'
|
import { GraphNavigation, User, Workspace, WorkspaceRole } from 'db'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { MembersList } from './MembersList'
|
import { MembersList } from './MembersList'
|
||||||
import { WorkspaceSettingsForm } from './WorkspaceSettingsForm'
|
import { WorkspaceSettingsForm } from './WorkspaceSettingsForm'
|
||||||
@ -44,9 +44,13 @@ export const WorkspaceSettingsModal = ({
|
|||||||
workspace,
|
workspace,
|
||||||
onClose,
|
onClose,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { canEdit } = useWorkspace()
|
const { currentRole } = useWorkspace()
|
||||||
const [selectedTab, setSelectedTab] = useState<SettingsTab>('my-account')
|
const [selectedTab, setSelectedTab] = useState<SettingsTab>('my-account')
|
||||||
|
|
||||||
|
console.log(currentRole)
|
||||||
|
|
||||||
|
const canEditWorkspace = currentRole === WorkspaceRole.ADMIN
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size="4xl">
|
<Modal isOpen={isOpen} onClose={onClose} size="4xl">
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
@ -94,7 +98,7 @@ export const WorkspaceSettingsModal = ({
|
|||||||
<Text pl="4" color="gray.500">
|
<Text pl="4" color="gray.500">
|
||||||
Workspace
|
Workspace
|
||||||
</Text>
|
</Text>
|
||||||
{canEdit && (
|
{canEditWorkspace && (
|
||||||
<Button
|
<Button
|
||||||
variant={
|
variant={
|
||||||
selectedTab === 'workspace-settings' ? 'solid' : 'ghost'
|
selectedTab === 'workspace-settings' ? 'solid' : 'ghost'
|
||||||
@ -124,7 +128,7 @@ export const WorkspaceSettingsModal = ({
|
|||||||
>
|
>
|
||||||
Members
|
Members
|
||||||
</Button>
|
</Button>
|
||||||
{canEdit && (
|
{canEditWorkspace && (
|
||||||
<Button
|
<Button
|
||||||
variant={selectedTab === 'billing' ? 'solid' : 'ghost'}
|
variant={selectedTab === 'billing' ? 'solid' : 'ghost'}
|
||||||
onClick={() => setSelectedTab('billing')}
|
onClick={() => setSelectedTab('billing')}
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export { WorkspaceSettingsModal } from './WorkspaceSettingsModal'
|
export * from './WorkspaceSettingsModal'
|
||||||
|
export * from './WorkspaceDropdown'
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
import { fetcher } from '@/utils/helpers'
|
|
||||||
import useSWR from 'swr'
|
|
||||||
import { WorkspaceWithMembers } from '../types'
|
|
||||||
|
|
||||||
export const useWorkspaces = ({ userId }: { userId?: string }) => {
|
|
||||||
const { data, error, mutate } = useSWR<
|
|
||||||
{
|
|
||||||
workspaces: WorkspaceWithMembers[]
|
|
||||||
},
|
|
||||||
Error
|
|
||||||
>(userId ? `/api/workspaces` : null, fetcher)
|
|
||||||
return {
|
|
||||||
workspaces: data?.workspaces,
|
|
||||||
isLoading: !error && !data,
|
|
||||||
mutate,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,2 +1,2 @@
|
|||||||
export { WorkspaceProvider, useWorkspace } from './WorkspaceProvider'
|
export { WorkspaceProvider, useWorkspace } from './WorkspaceProvider'
|
||||||
export { WorkspaceSettingsModal } from './components/WorkspaceSettingsModal'
|
export * from './components'
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
import { Workspace } from 'db'
|
|
||||||
import { sendRequest } from 'utils'
|
|
||||||
|
|
||||||
export const createWorkspaceQuery = async (
|
|
||||||
body: Omit<
|
|
||||||
Workspace,
|
|
||||||
| 'id'
|
|
||||||
| 'icon'
|
|
||||||
| 'createdAt'
|
|
||||||
| 'stripeId'
|
|
||||||
| 'additionalChatsIndex'
|
|
||||||
| 'additionalStorageIndex'
|
|
||||||
| 'chatsLimitFirstEmailSentAt'
|
|
||||||
| 'chatsLimitSecondEmailSentAt'
|
|
||||||
| 'storageLimitFirstEmailSentAt'
|
|
||||||
| 'storageLimitSecondEmailSentAt'
|
|
||||||
| 'customChatsLimit'
|
|
||||||
| 'customStorageLimit'
|
|
||||||
| 'customSeatsLimit'
|
|
||||||
>
|
|
||||||
) =>
|
|
||||||
sendRequest<{
|
|
||||||
workspace: Workspace
|
|
||||||
}>({
|
|
||||||
url: `/api/workspaces`,
|
|
||||||
method: 'POST',
|
|
||||||
body,
|
|
||||||
})
|
|
@ -1,10 +0,0 @@
|
|||||||
import { Workspace } from 'db'
|
|
||||||
import { sendRequest } from 'utils'
|
|
||||||
|
|
||||||
export const deleteWorkspaceQuery = (workspaceId: string) =>
|
|
||||||
sendRequest<{
|
|
||||||
workspace: Workspace
|
|
||||||
}>({
|
|
||||||
url: `/api/workspaces/${workspaceId}`,
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
@ -1,11 +0,0 @@
|
|||||||
import { Workspace } from 'db'
|
|
||||||
import { sendRequest } from 'utils'
|
|
||||||
|
|
||||||
export const updateWorkspaceQuery = async (updates: Partial<Workspace>) =>
|
|
||||||
sendRequest<{
|
|
||||||
workspace: Workspace
|
|
||||||
}>({
|
|
||||||
url: `/api/workspaces/${updates.id}`,
|
|
||||||
method: 'PATCH',
|
|
||||||
body: updates,
|
|
||||||
})
|
|
@ -1,9 +1,7 @@
|
|||||||
import { MemberInWorkspace, Workspace } from 'db'
|
import { MemberInWorkspace } from 'db'
|
||||||
|
|
||||||
export type Member = MemberInWorkspace & {
|
export type Member = MemberInWorkspace & {
|
||||||
name: string | null
|
name: string | null
|
||||||
image: string | null
|
image: string | null
|
||||||
email: string | null
|
email: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkspaceWithMembers = Workspace & { members: MemberInWorkspace[] }
|
|
||||||
|
2
apps/builder/src/features/workspace/utils/index.ts
Normal file
2
apps/builder/src/features/workspace/utils/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './parseNewName'
|
||||||
|
export * from './setWorkspaceIdInLocalStorage'
|
17
apps/builder/src/features/workspace/utils/parseNewName.ts
Normal file
17
apps/builder/src/features/workspace/utils/parseNewName.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Workspace } from 'models'
|
||||||
|
|
||||||
|
export const parseNewName = (
|
||||||
|
userFullName: string | undefined,
|
||||||
|
existingWorkspaces: Pick<Workspace, 'name'>[]
|
||||||
|
) => {
|
||||||
|
const workspaceName = userFullName
|
||||||
|
? `${userFullName}'s workspace`
|
||||||
|
: 'My workspace'
|
||||||
|
let newName = workspaceName
|
||||||
|
let i = 1
|
||||||
|
while (existingWorkspaces.find((w) => w.name === newName)) {
|
||||||
|
newName = `${workspaceName} (${i})`
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return newName
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
export const setWorkspaceIdInLocalStorage = (workspaceId: string) => {
|
||||||
|
localStorage.setItem('workspaceId', workspaceId)
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { httpBatchLink } from '@trpc/client'
|
import { httpBatchLink } from '@trpc/client'
|
||||||
import { createTRPCNext } from '@trpc/next'
|
import { createTRPCNext } from '@trpc/next'
|
||||||
import type { AppRouter } from '../utils/server/routers/v1/_app'
|
import type { AppRouter } from '../utils/server/routers/v1/trpcRouter'
|
||||||
import superjson from 'superjson'
|
import superjson from 'superjson'
|
||||||
|
|
||||||
const getBaseUrl = () =>
|
const getBaseUrl = () =>
|
||||||
|
@ -55,7 +55,7 @@ const App = ({
|
|||||||
<UserProvider>
|
<UserProvider>
|
||||||
{typebotId ? (
|
{typebotId ? (
|
||||||
<TypebotProvider typebotId={typebotId}>
|
<TypebotProvider typebotId={typebotId}>
|
||||||
<WorkspaceProvider>
|
<WorkspaceProvider typebotId={typebotId}>
|
||||||
<Component />
|
<Component />
|
||||||
<SupportBubble />
|
<SupportBubble />
|
||||||
</WorkspaceProvider>
|
</WorkspaceProvider>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { createContext } from '@/utils/server/context'
|
import { createContext } from '@/utils/server/context'
|
||||||
import { appRouter } from '@/utils/server/routers/v1/_app'
|
import { trpcRouter } from '@/utils/server/routers/v1/trpcRouter'
|
||||||
import { captureException } from '@sentry/nextjs'
|
import { captureException } from '@sentry/nextjs'
|
||||||
import { createNextApiHandler } from '@trpc/server/adapters/next'
|
import { createNextApiHandler } from '@trpc/server/adapters/next'
|
||||||
|
|
||||||
export default createNextApiHandler({
|
export default createNextApiHandler({
|
||||||
router: appRouter,
|
router: trpcRouter,
|
||||||
createContext,
|
createContext,
|
||||||
onError({ error }) {
|
onError({ error }) {
|
||||||
if (error.code === 'INTERNAL_SERVER_ERROR') {
|
if (error.code === 'INTERNAL_SERVER_ERROR') {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { createContext } from '@/utils/server/context'
|
import { createContext } from '@/utils/server/context'
|
||||||
import { appRouter } from '@/utils/server/routers/v1/_app'
|
import { trpcRouter } from '@/utils/server/routers/v1/trpcRouter'
|
||||||
import { captureException } from '@sentry/nextjs'
|
import { captureException } from '@sentry/nextjs'
|
||||||
import { createOpenApiNextHandler } from 'trpc-openapi'
|
import { createOpenApiNextHandler } from 'trpc-openapi'
|
||||||
|
|
||||||
export default createOpenApiNextHandler({
|
export default createOpenApiNextHandler({
|
||||||
router: appRouter,
|
router: trpcRouter,
|
||||||
createContext,
|
createContext,
|
||||||
onError({ error }) {
|
onError({ error }) {
|
||||||
if (error.code === 'INTERNAL_SERVER_ERROR') {
|
if (error.code === 'INTERNAL_SERVER_ERROR') {
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
import { withSentry } from '@sentry/nextjs'
|
|
||||||
import { Plan, Workspace } from 'db'
|
|
||||||
import prisma from '@/lib/prisma'
|
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
import { methodNotAllowed, notAuthenticated } from 'utils/api'
|
|
||||||
import { getAuthenticatedUser } from '@/features/auth/api'
|
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|
||||||
const user = await getAuthenticatedUser(req)
|
|
||||||
if (!user) return notAuthenticated(res)
|
|
||||||
if (req.method === 'GET') {
|
|
||||||
const workspaces = await prisma.workspace.findMany({
|
|
||||||
where: { members: { some: { userId: user.id } } },
|
|
||||||
include: { members: true },
|
|
||||||
orderBy: { createdAt: 'asc' },
|
|
||||||
})
|
|
||||||
return res.send({ workspaces })
|
|
||||||
}
|
|
||||||
if (req.method === 'POST') {
|
|
||||||
const data = req.body as Workspace
|
|
||||||
const workspace = await prisma.workspace.create({
|
|
||||||
data: {
|
|
||||||
...data,
|
|
||||||
members: { create: [{ role: 'ADMIN', userId: user.id }] },
|
|
||||||
plan:
|
|
||||||
process.env.ADMIN_EMAIL === user.email ? Plan.LIFETIME : Plan.FREE,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return res.status(200).json({
|
|
||||||
workspace,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
methodNotAllowed(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withSentry(handler)
|
|
@ -1,41 +0,0 @@
|
|||||||
import { withSentry } from '@sentry/nextjs'
|
|
||||||
import { Prisma, Workspace, WorkspaceRole } from 'db'
|
|
||||||
import prisma from '@/lib/prisma'
|
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
import { getAuthenticatedUser } from '@/features/auth/api'
|
|
||||||
import { methodNotAllowed, notAuthenticated } from 'utils/api'
|
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|
||||||
const user = await getAuthenticatedUser(req)
|
|
||||||
if (!user) return notAuthenticated(res)
|
|
||||||
if (req.method === 'PATCH') {
|
|
||||||
const id = req.query.workspaceId as string
|
|
||||||
const updates = req.body as Partial<Workspace>
|
|
||||||
const updatedWorkspace = await prisma.workspace.updateMany({
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
|
|
||||||
},
|
|
||||||
data: updates,
|
|
||||||
})
|
|
||||||
return res.status(200).json({
|
|
||||||
workspace: updatedWorkspace,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (req.method === 'DELETE') {
|
|
||||||
const id = req.query.workspaceId as string
|
|
||||||
const workspaceFilter: Prisma.WorkspaceWhereInput = {
|
|
||||||
id,
|
|
||||||
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
|
|
||||||
}
|
|
||||||
await prisma.workspace.deleteMany({
|
|
||||||
where: workspaceFilter,
|
|
||||||
})
|
|
||||||
return res.status(200).json({
|
|
||||||
message: 'success',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
methodNotAllowed(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withSentry(handler)
|
|
@ -11,7 +11,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
const workspaceId = req.query.workspaceId as string
|
const workspaceId = req.query.workspaceId as string
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
|
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
|
||||||
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0)
|
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1)
|
||||||
const totalChatsUsed = await prisma.result.count({
|
const totalChatsUsed = await prisma.result.count({
|
||||||
where: {
|
where: {
|
||||||
typebot: {
|
typebot: {
|
||||||
|
375
apps/builder/src/test/assets/typebots/api.json
Normal file
375
apps/builder/src/test/assets/typebots/api.json
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
{
|
||||||
|
"id": "qujHPjZ44xbrHb1hS1d8qC",
|
||||||
|
"createdAt": "2022-02-05T06:21:16.522Z",
|
||||||
|
"updatedAt": "2022-02-05T06:21:16.522Z",
|
||||||
|
"name": "My typebot",
|
||||||
|
"publishedTypebotId": null,
|
||||||
|
"folderId": null,
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"id": "k6kY6gwRE6noPoYQNGzgUq",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "22HP69iipkLjJDTUcc1AWW",
|
||||||
|
"type": "start",
|
||||||
|
"label": "Start",
|
||||||
|
"groupId": "k6kY6gwRE6noPoYQNGzgUq",
|
||||||
|
"outgoingEdgeId": "oNvqaqNExdSH2kKEhKZHuE"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Start",
|
||||||
|
"graphCoordinates": { "x": 0, "y": 0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "kinRXxYop2X4d7F9qt8WNB",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "sc1y8VwDabNJgiVTBi4qtif",
|
||||||
|
"type": "text",
|
||||||
|
"groupId": "kinRXxYop2X4d7F9qt8WNB",
|
||||||
|
"content": {
|
||||||
|
"html": "<div>Welcome to <span class=\"slate-bold\">AA</span> (Awesome Agency)</div>",
|
||||||
|
"richText": [
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"children": [
|
||||||
|
{ "text": "Welcome to " },
|
||||||
|
{ "bold": true, "text": "AA" },
|
||||||
|
{ "text": " (Awesome Agency)" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plainText": "Welcome to AA (Awesome Agency)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s7YqZTBeyCa4Hp3wN2j922c",
|
||||||
|
"type": "image",
|
||||||
|
"groupId": "kinRXxYop2X4d7F9qt8WNB",
|
||||||
|
"content": {
|
||||||
|
"url": "https://media2.giphy.com/media/XD9o33QG9BoMis7iM4/giphy.gif?cid=fe3852a3ihg8rvipzzky5lybmdyq38fhke2tkrnshwk52c7d&rid=giphy.gif&ct=g"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sbjZWLJGVkHAkDqS4JQeGow",
|
||||||
|
"type": "choice input",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "hQw2zbp7FDX7XYK9cFpbgC",
|
||||||
|
"type": 0,
|
||||||
|
"blockId": "sbjZWLJGVkHAkDqS4JQeGow",
|
||||||
|
"content": "Hi!"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"groupId": "kinRXxYop2X4d7F9qt8WNB",
|
||||||
|
"options": { "buttonLabel": "Send", "isMultipleChoice": false },
|
||||||
|
"outgoingEdgeId": "i51YhHpk1dtSyduFNf5Wim"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Welcome",
|
||||||
|
"graphCoordinates": { "x": 1, "y": 148 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "o4SH1UtKANnW5N5D67oZUz",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "sxeYubYN6XzhAfG7m9Fivhc",
|
||||||
|
"type": "text",
|
||||||
|
"groupId": "o4SH1UtKANnW5N5D67oZUz",
|
||||||
|
"content": {
|
||||||
|
"html": "<div>Great! Nice to meet you {{Name}}</div>",
|
||||||
|
"richText": [
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"children": [{ "text": "Great! Nice to meet you {{Name}}" }]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plainText": "Great! Nice to meet you {{Name}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "scQ5kduafAtfP9T8SHUJnGi",
|
||||||
|
"type": "text",
|
||||||
|
"groupId": "o4SH1UtKANnW5N5D67oZUz",
|
||||||
|
"content": {
|
||||||
|
"html": "<div>What's the best email we can reach you at?</div>",
|
||||||
|
"richText": [
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"children": [
|
||||||
|
{ "text": "What's the best email we can reach you at?" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plainText": "What's the best email we can reach you at?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "snbsad18Bgry8yZ8DZCfdFD",
|
||||||
|
"type": "email input",
|
||||||
|
"groupId": "o4SH1UtKANnW5N5D67oZUz",
|
||||||
|
"options": {
|
||||||
|
"labels": { "button": "Send", "placeholder": "Type your email..." },
|
||||||
|
"variableId": "3VFChNVSCXQ2rXv4DrJ8Ah"
|
||||||
|
},
|
||||||
|
"outgoingEdgeId": "w3MiN1Ct38jT5NykVsgmb5"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Email",
|
||||||
|
"graphCoordinates": { "x": 669, "y": 141 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "q5dAhqSTCaNdiGSJm9B9Rw",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "sgtE2Sy7cKykac9B223Kq9R",
|
||||||
|
"type": "text",
|
||||||
|
"groupId": "q5dAhqSTCaNdiGSJm9B9Rw",
|
||||||
|
"content": {
|
||||||
|
"html": "<div>What's your name?</div>",
|
||||||
|
"richText": [
|
||||||
|
{ "type": "p", "children": [{ "text": "What's your name?" }] }
|
||||||
|
],
|
||||||
|
"plainText": "What's your name?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sqEsMo747LTDnY9FjQcEwUv",
|
||||||
|
"type": "text input",
|
||||||
|
"groupId": "q5dAhqSTCaNdiGSJm9B9Rw",
|
||||||
|
"options": {
|
||||||
|
"isLong": false,
|
||||||
|
"labels": {
|
||||||
|
"button": "Send",
|
||||||
|
"placeholder": "Type your answer..."
|
||||||
|
},
|
||||||
|
"variableId": "giiLFGw5xXBCHzvp1qAbdX"
|
||||||
|
},
|
||||||
|
"outgoingEdgeId": "4tYbERpi5Po4goVgt6rWXg"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Name",
|
||||||
|
"graphCoordinates": { "x": 340, "y": 143 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fKqRz7iswk7ULaj5PJocZL",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "su7HceVXWyTCzi2vv3m4QbK",
|
||||||
|
"type": "text",
|
||||||
|
"groupId": "fKqRz7iswk7ULaj5PJocZL",
|
||||||
|
"content": {
|
||||||
|
"html": "<div>What services are you interested in?</div>",
|
||||||
|
"richText": [
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"children": [{ "text": "What services are you interested in?" }]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plainText": "What services are you interested in?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s5VQGsVF4hQgziQsXVdwPDW",
|
||||||
|
"type": "choice input",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "fnLCBF4NdraSwcubnBhk8H",
|
||||||
|
"type": 0,
|
||||||
|
"blockId": "s5VQGsVF4hQgziQsXVdwPDW",
|
||||||
|
"content": "Website dev"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "a782h8ynMouY84QjH7XSnR",
|
||||||
|
"type": 0,
|
||||||
|
"blockId": "s5VQGsVF4hQgziQsXVdwPDW",
|
||||||
|
"content": "Content Marketing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "jGvh94zBByvVFpSS3w97zY",
|
||||||
|
"type": 0,
|
||||||
|
"blockId": "s5VQGsVF4hQgziQsXVdwPDW",
|
||||||
|
"content": "Social Media"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6PRLbKUezuFmwWtLVbvAQ7",
|
||||||
|
"type": 0,
|
||||||
|
"blockId": "s5VQGsVF4hQgziQsXVdwPDW",
|
||||||
|
"content": "UI / UX Design"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"groupId": "fKqRz7iswk7ULaj5PJocZL",
|
||||||
|
"options": { "buttonLabel": "Send", "isMultipleChoice": true },
|
||||||
|
"outgoingEdgeId": "ohTRakmcYJ7GdFWRZrWRjk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Services",
|
||||||
|
"graphCoordinates": { "x": 1002, "y": 144 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "7qHBEyCMvKEJryBHzPmHjV",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "sqR8Sz9gW21aUYKtUikq7qZ",
|
||||||
|
"type": "text",
|
||||||
|
"groupId": "7qHBEyCMvKEJryBHzPmHjV",
|
||||||
|
"content": {
|
||||||
|
"html": "<div>Can you tell me a bit more about your needs?</div>",
|
||||||
|
"richText": [
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"children": [
|
||||||
|
{ "text": "Can you tell me a bit more about your needs?" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plainText": "Can you tell me a bit more about your needs?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sqFy2G3C1mh9p6s3QBdSS5x",
|
||||||
|
"type": "text input",
|
||||||
|
"groupId": "7qHBEyCMvKEJryBHzPmHjV",
|
||||||
|
"options": {
|
||||||
|
"isLong": true,
|
||||||
|
"labels": { "button": "Send", "placeholder": "Type your answer..." }
|
||||||
|
},
|
||||||
|
"outgoingEdgeId": "sH5nUssG2XQbm6ZidGv9BY"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Additional information",
|
||||||
|
"graphCoordinates": { "x": 1337, "y": 145 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "vF7AD7zSAj7SNvN3gr9N94",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "seLegenCgUwMopRFeAefqZ7",
|
||||||
|
"type": "text",
|
||||||
|
"groupId": "vF7AD7zSAj7SNvN3gr9N94",
|
||||||
|
"content": {
|
||||||
|
"html": "<div>Perfect!</div>",
|
||||||
|
"richText": [{ "type": "p", "children": [{ "text": "Perfect!" }] }],
|
||||||
|
"plainText": "Perfect!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s779Q1y51aVaDUJVrFb16vv",
|
||||||
|
"type": "text",
|
||||||
|
"groupId": "vF7AD7zSAj7SNvN3gr9N94",
|
||||||
|
"content": {
|
||||||
|
"html": "<div>We'll get back to you at {{Email}}</div>",
|
||||||
|
"richText": [
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"children": [{ "text": "We'll get back to you at {{Email}}" }]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plainText": "We'll get back to you at {{Email}}"
|
||||||
|
},
|
||||||
|
"outgoingEdgeId": "fTVo43AG97eKcaTrZf9KyV"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Bye",
|
||||||
|
"graphCoordinates": { "x": 1668, "y": 143 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "webhookGroup",
|
||||||
|
"graphCoordinates": { "x": 1996, "y": 134 },
|
||||||
|
"title": "Webhook",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "webhookBlock",
|
||||||
|
"groupId": "webhookGroup",
|
||||||
|
"type": "Webhook",
|
||||||
|
"options": { "responseVariableMapping": [], "variablesForTest": [] },
|
||||||
|
"webhookId": "webhook1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variables": [
|
||||||
|
{ "id": "giiLFGw5xXBCHzvp1qAbdX", "name": "Name" },
|
||||||
|
{ "id": "3VFChNVSCXQ2rXv4DrJ8Ah", "name": "Email" }
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"id": "oNvqaqNExdSH2kKEhKZHuE",
|
||||||
|
"to": { "groupId": "kinRXxYop2X4d7F9qt8WNB" },
|
||||||
|
"from": {
|
||||||
|
"blockId": "22HP69iipkLjJDTUcc1AWW",
|
||||||
|
"groupId": "k6kY6gwRE6noPoYQNGzgUq"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "i51YhHpk1dtSyduFNf5Wim",
|
||||||
|
"to": { "groupId": "q5dAhqSTCaNdiGSJm9B9Rw" },
|
||||||
|
"from": {
|
||||||
|
"blockId": "sbjZWLJGVkHAkDqS4JQeGow",
|
||||||
|
"groupId": "kinRXxYop2X4d7F9qt8WNB"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4tYbERpi5Po4goVgt6rWXg",
|
||||||
|
"to": { "groupId": "o4SH1UtKANnW5N5D67oZUz" },
|
||||||
|
"from": {
|
||||||
|
"blockId": "sqEsMo747LTDnY9FjQcEwUv",
|
||||||
|
"groupId": "q5dAhqSTCaNdiGSJm9B9Rw"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "w3MiN1Ct38jT5NykVsgmb5",
|
||||||
|
"to": { "groupId": "fKqRz7iswk7ULaj5PJocZL" },
|
||||||
|
"from": {
|
||||||
|
"blockId": "snbsad18Bgry8yZ8DZCfdFD",
|
||||||
|
"groupId": "o4SH1UtKANnW5N5D67oZUz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ohTRakmcYJ7GdFWRZrWRjk",
|
||||||
|
"to": { "groupId": "7qHBEyCMvKEJryBHzPmHjV" },
|
||||||
|
"from": {
|
||||||
|
"blockId": "s5VQGsVF4hQgziQsXVdwPDW",
|
||||||
|
"groupId": "fKqRz7iswk7ULaj5PJocZL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sH5nUssG2XQbm6ZidGv9BY",
|
||||||
|
"to": { "groupId": "vF7AD7zSAj7SNvN3gr9N94" },
|
||||||
|
"from": {
|
||||||
|
"blockId": "sqFy2G3C1mh9p6s3QBdSS5x",
|
||||||
|
"groupId": "7qHBEyCMvKEJryBHzPmHjV"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": {
|
||||||
|
"groupId": "vF7AD7zSAj7SNvN3gr9N94",
|
||||||
|
"blockId": "s779Q1y51aVaDUJVrFb16vv"
|
||||||
|
},
|
||||||
|
"to": { "groupId": "webhookGroup" },
|
||||||
|
"id": "fTVo43AG97eKcaTrZf9KyV"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme": {
|
||||||
|
"chat": {
|
||||||
|
"inputs": {
|
||||||
|
"color": "#303235",
|
||||||
|
"backgroundColor": "#FFFFFF",
|
||||||
|
"placeholderColor": "#9095A0"
|
||||||
|
},
|
||||||
|
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
|
||||||
|
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
|
||||||
|
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
|
||||||
|
},
|
||||||
|
"general": { "font": "Open Sans", "background": { "type": "None" } }
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"general": { "isBrandingEnabled": true },
|
||||||
|
"metadata": {
|
||||||
|
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
|
||||||
|
},
|
||||||
|
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
|
||||||
|
},
|
||||||
|
"publicId": null,
|
||||||
|
"customDomain": null
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import { generateOpenApiDocument } from 'trpc-openapi'
|
import { generateOpenApiDocument } from 'trpc-openapi'
|
||||||
import { writeFileSync } from 'fs'
|
import { writeFileSync } from 'fs'
|
||||||
import { appRouter } from './routers/v1/_app'
|
import { trpcRouter } from './routers/v1/trpcRouter'
|
||||||
|
|
||||||
const openApiDocument = generateOpenApiDocument(appRouter, {
|
const openApiDocument = generateOpenApiDocument(trpcRouter, {
|
||||||
title: 'Builder API',
|
title: 'Builder API',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
baseUrl: 'https://app.typebot.io/api/v1',
|
baseUrl: 'https://app.typebot.io/api/v1',
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
import { resultsRouter } from '@/features/results/api'
|
|
||||||
import { router } from '../../trpc'
|
|
||||||
|
|
||||||
export const appRouter = router({
|
|
||||||
results: resultsRouter,
|
|
||||||
})
|
|
||||||
|
|
||||||
export type AppRouter = typeof appRouter
|
|
14
apps/builder/src/utils/server/routers/v1/trpcRouter.ts
Normal file
14
apps/builder/src/utils/server/routers/v1/trpcRouter.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { webhookRouter } from '@/features/blocks/integrations/webhook/api'
|
||||||
|
import { resultsRouter } from '@/features/results/api'
|
||||||
|
import { typebotRouter } from '@/features/typebot/api'
|
||||||
|
import { workspaceRouter } from '@/features/workspace/api'
|
||||||
|
import { router } from '../../trpc'
|
||||||
|
|
||||||
|
export const trpcRouter = router({
|
||||||
|
workspace: workspaceRouter,
|
||||||
|
typebot: typebotRouter,
|
||||||
|
webhook: webhookRouter,
|
||||||
|
results: resultsRouter,
|
||||||
|
})
|
||||||
|
|
||||||
|
export type AppRouter = typeof trpcRouter
|
File diff suppressed because it is too large
Load Diff
@ -22,8 +22,8 @@
|
|||||||
"@docusaurus/theme-search-algolia": "2.2.0",
|
"@docusaurus/theme-search-algolia": "2.2.0",
|
||||||
"@mdx-js/react": "1.6.22",
|
"@mdx-js/react": "1.6.22",
|
||||||
"@svgr/webpack": "6.5.1",
|
"@svgr/webpack": "6.5.1",
|
||||||
"clsx": "1.2.1",
|
|
||||||
"@typebot.io/docusaurus-preset-openapi": "0.6.5",
|
"@typebot.io/docusaurus-preset-openapi": "0.6.5",
|
||||||
|
"clsx": "1.2.1",
|
||||||
"file-loader": "6.2.0",
|
"file-loader": "6.2.0",
|
||||||
"prism-react-renderer": "1.3.5",
|
"prism-react-renderer": "1.3.5",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
|
@ -29,8 +29,8 @@
|
|||||||
"@types/react-scroll": "1.8.5",
|
"@types/react-scroll": "1.8.5",
|
||||||
"@types/react-transition-group": "4.4.5",
|
"@types/react-transition-group": "4.4.5",
|
||||||
"autoprefixer": "10.4.13",
|
"autoprefixer": "10.4.13",
|
||||||
"tsup": "6.5.0",
|
|
||||||
"db": "workspace:*",
|
"db": "workspace:*",
|
||||||
|
"esbuild": "^0.15.16",
|
||||||
"eslint": "8.28.0",
|
"eslint": "8.28.0",
|
||||||
"eslint-config-custom": "workspace:*",
|
"eslint-config-custom": "workspace:*",
|
||||||
"models": "workspace:*",
|
"models": "workspace:*",
|
||||||
@ -39,10 +39,11 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"tailwindcss": "3.2.4",
|
"tailwindcss": "3.2.4",
|
||||||
"typescript": "4.9.3",
|
"tsconfig": "workspace:*",
|
||||||
"utils": "workspace:*",
|
"tsup": "6.5.0",
|
||||||
"typebot-js": "workspace:*",
|
"typebot-js": "workspace:*",
|
||||||
"tsconfig": "workspace:*"
|
"typescript": "4.9.3",
|
||||||
|
"utils": "workspace:*"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"db": "workspace:*",
|
"db": "workspace:*",
|
||||||
|
60
packages/models/src/features/workspace.ts
Normal file
60
packages/models/src/features/workspace.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { schemaForType } from './utils'
|
||||||
|
import {
|
||||||
|
Workspace as WorkspacePrisma,
|
||||||
|
Plan,
|
||||||
|
MemberInWorkspace as MemberInWorkspacePrisma,
|
||||||
|
WorkspaceRole,
|
||||||
|
User as UserPrisma,
|
||||||
|
WorkspaceInvitation as WorkspaceInvitationPrisma,
|
||||||
|
} from 'db'
|
||||||
|
|
||||||
|
export const workspaceMemberSchema = schemaForType<
|
||||||
|
Omit<MemberInWorkspacePrisma, 'userId'> & {
|
||||||
|
user: Pick<UserPrisma, 'name' | 'email' | 'image'>
|
||||||
|
}
|
||||||
|
>()(
|
||||||
|
z.object({
|
||||||
|
workspaceId: z.string(),
|
||||||
|
user: z.object({
|
||||||
|
name: z.string().nullable(),
|
||||||
|
email: z.string().nullable(),
|
||||||
|
image: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
role: z.nativeEnum(WorkspaceRole),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const workspaceInvitationSchema = schemaForType<
|
||||||
|
Omit<WorkspaceInvitationPrisma, 'workspaceId' | 'userId' | 'id'>
|
||||||
|
>()(
|
||||||
|
z.object({
|
||||||
|
email: z.string(),
|
||||||
|
type: z.nativeEnum(WorkspaceRole),
|
||||||
|
createdAt: z.date(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const workspaceSchema = schemaForType<WorkspacePrisma>()(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
icon: z.string().nullable(),
|
||||||
|
plan: z.nativeEnum(Plan),
|
||||||
|
stripeId: z.string().nullable(),
|
||||||
|
additionalChatsIndex: z.number(),
|
||||||
|
additionalStorageIndex: z.number(),
|
||||||
|
chatsLimitFirstEmailSentAt: z.date().nullable(),
|
||||||
|
chatsLimitSecondEmailSentAt: z.date().nullable(),
|
||||||
|
storageLimitFirstEmailSentAt: z.date().nullable(),
|
||||||
|
storageLimitSecondEmailSentAt: z.date().nullable(),
|
||||||
|
customChatsLimit: z.number().nullable(),
|
||||||
|
customStorageLimit: z.number().nullable(),
|
||||||
|
customSeatsLimit: z.number().nullable(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export type Workspace = z.infer<typeof workspaceSchema>
|
||||||
|
export type WorkspaceMember = z.infer<typeof workspaceMemberSchema>
|
||||||
|
export type WorkspaceInvitation = z.infer<typeof workspaceInvitationSchema>
|
@ -7,3 +7,4 @@ export * from './features/utils'
|
|||||||
export * from './features/credentials'
|
export * from './features/credentials'
|
||||||
export * from './features/webhooks'
|
export * from './features/webhooks'
|
||||||
export * from './features/chat'
|
export * from './features/chat'
|
||||||
|
export * from './features/workspace'
|
||||||
|
246
pnpm-lock.yaml
generated
246
pnpm-lock.yaml
generated
@ -434,6 +434,7 @@ importers:
|
|||||||
'@types/react-transition-group': 4.4.5
|
'@types/react-transition-group': 4.4.5
|
||||||
autoprefixer: 10.4.13
|
autoprefixer: 10.4.13
|
||||||
db: workspace:*
|
db: workspace:*
|
||||||
|
esbuild: ^0.15.16
|
||||||
eslint: 8.28.0
|
eslint: 8.28.0
|
||||||
eslint-config-custom: workspace:*
|
eslint-config-custom: workspace:*
|
||||||
models: workspace:*
|
models: workspace:*
|
||||||
@ -473,6 +474,7 @@ importers:
|
|||||||
'@types/react-transition-group': 4.4.5
|
'@types/react-transition-group': 4.4.5
|
||||||
autoprefixer: 10.4.13_postcss@8.4.19
|
autoprefixer: 10.4.13_postcss@8.4.19
|
||||||
db: link:../db
|
db: link:../db
|
||||||
|
esbuild: 0.15.16
|
||||||
eslint: 8.28.0
|
eslint: 8.28.0
|
||||||
eslint-config-custom: link:../eslint-config-custom
|
eslint-config-custom: link:../eslint-config-custom
|
||||||
models: link:../models
|
models: link:../models
|
||||||
@ -4131,7 +4133,7 @@ packages:
|
|||||||
/@esbuild-kit/core-utils/3.0.0:
|
/@esbuild-kit/core-utils/3.0.0:
|
||||||
resolution: {integrity: sha512-TXmwH9EFS3DC2sI2YJWJBgHGhlteK0Xyu1VabwetMULfm3oYhbrsWV5yaSr2NTWZIgDGVLHbRf0inxbjXqAcmQ==}
|
resolution: {integrity: sha512-TXmwH9EFS3DC2sI2YJWJBgHGhlteK0Xyu1VabwetMULfm3oYhbrsWV5yaSr2NTWZIgDGVLHbRf0inxbjXqAcmQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.15.10
|
esbuild: 0.15.16
|
||||||
source-map-support: 0.5.21
|
source-map-support: 0.5.21
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@ -4151,6 +4153,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/@esbuild/android-arm/0.15.16:
|
||||||
|
resolution: {integrity: sha512-nyB6CH++2mSgx3GbnrJsZSxzne5K0HMyNIWafDHqYy7IwxFc4fd/CgHVZXr8Eh+Q3KbIAcAe3vGyqIPhGblvMQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/@esbuild/linux-loong64/0.15.10:
|
/@esbuild/linux-loong64/0.15.10:
|
||||||
resolution: {integrity: sha512-w0Ou3Z83LOYEkwaui2M8VwIp+nLi/NA60lBLMvaJ+vXVMcsARYdEzLNE7RSm4+lSg4zq4d7fAVuzk7PNQ5JFgg==}
|
resolution: {integrity: sha512-w0Ou3Z83LOYEkwaui2M8VwIp+nLi/NA60lBLMvaJ+vXVMcsARYdEzLNE7RSm4+lSg4zq4d7fAVuzk7PNQ5JFgg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -4160,6 +4171,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/@esbuild/linux-loong64/0.15.16:
|
||||||
|
resolution: {integrity: sha512-SDLfP1uoB0HZ14CdVYgagllgrG7Mdxhkt4jDJOKl/MldKrkQ6vDJMZKl2+5XsEY/Lzz37fjgLQoJBGuAw/x8kQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [loong64]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/@eslint/eslintrc/1.3.3:
|
/@eslint/eslintrc/1.3.3:
|
||||||
resolution: {integrity: sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==}
|
resolution: {integrity: sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
@ -7590,6 +7610,16 @@ packages:
|
|||||||
load-tsconfig: 0.2.3
|
load-tsconfig: 0.2.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/bundle-require/3.1.2_esbuild@0.15.16:
|
||||||
|
resolution: {integrity: sha512-Of6l6JBAxiyQ5axFxUM6dYeP/W7X2Sozeo/4EYB9sJhL+dqL7TKjg+shwxp6jlu/6ZSERfsYtIpSJ1/x3XkAEA==}
|
||||||
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
esbuild: '>=0.13'
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.15.16
|
||||||
|
load-tsconfig: 0.2.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
/bytes/3.0.0:
|
/bytes/3.0.0:
|
||||||
resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==}
|
resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@ -9238,6 +9268,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-android-64/0.15.16:
|
||||||
|
resolution: {integrity: sha512-Vwkv/sT0zMSgPSVO3Jlt1pUbnZuOgtOQJkJkyyJFAlLe7BiT8e9ESzo0zQSx4c3wW4T6kGChmKDPMbWTgtliQA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [android]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/esbuild-android-arm64/0.15.10:
|
/esbuild-android-arm64/0.15.10:
|
||||||
resolution: {integrity: sha512-EOt55D6xBk5O05AK8brXUbZmoFj4chM8u3riGflLa6ziEoVvNjRdD7Cnp82NHQGfSHgYR06XsPI8/sMuA/cUwg==}
|
resolution: {integrity: sha512-EOt55D6xBk5O05AK8brXUbZmoFj4chM8u3riGflLa6ziEoVvNjRdD7Cnp82NHQGfSHgYR06XsPI8/sMuA/cUwg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -9247,6 +9286,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-android-arm64/0.15.16:
|
||||||
|
resolution: {integrity: sha512-lqfKuofMExL5niNV3gnhMUYacSXfsvzTa/58sDlBET/hCOG99Zmeh+lz6kvdgvGOsImeo6J9SW21rFCogNPLxg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/esbuild-darwin-64/0.15.10:
|
/esbuild-darwin-64/0.15.10:
|
||||||
resolution: {integrity: sha512-hbDJugTicqIm+WKZgp208d7FcXcaK8j2c0l+fqSJ3d2AzQAfjEYDRM3Z2oMeqSJ9uFxyj/muSACLdix7oTstRA==}
|
resolution: {integrity: sha512-hbDJugTicqIm+WKZgp208d7FcXcaK8j2c0l+fqSJ3d2AzQAfjEYDRM3Z2oMeqSJ9uFxyj/muSACLdix7oTstRA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -9256,6 +9304,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-darwin-64/0.15.16:
|
||||||
|
resolution: {integrity: sha512-wo2VWk/n/9V2TmqUZ/KpzRjCEcr00n7yahEdmtzlrfQ3lfMCf3Wa+0sqHAbjk3C6CKkR3WKK/whkMq5Gj4Da9g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/esbuild-darwin-arm64/0.15.10:
|
/esbuild-darwin-arm64/0.15.10:
|
||||||
resolution: {integrity: sha512-M1t5+Kj4IgSbYmunf2BB6EKLkWUq+XlqaFRiGOk8bmBapu9bCDrxjf4kUnWn59Dka3I27EiuHBKd1rSO4osLFQ==}
|
resolution: {integrity: sha512-M1t5+Kj4IgSbYmunf2BB6EKLkWUq+XlqaFRiGOk8bmBapu9bCDrxjf4kUnWn59Dka3I27EiuHBKd1rSO4osLFQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -9265,6 +9322,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-darwin-arm64/0.15.16:
|
||||||
|
resolution: {integrity: sha512-fMXaUr5ou0M4WnewBKsspMtX++C1yIa3nJ5R2LSbLCfJT3uFdcRoU/NZjoM4kOMKyOD9Sa/2vlgN8G07K3SJnw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/esbuild-freebsd-64/0.15.10:
|
/esbuild-freebsd-64/0.15.10:
|
||||||
resolution: {integrity: sha512-KMBFMa7C8oc97nqDdoZwtDBX7gfpolkk6Bcmj6YFMrtCMVgoU/x2DI1p74DmYl7CSS6Ppa3xgemrLrr5IjIn0w==}
|
resolution: {integrity: sha512-KMBFMa7C8oc97nqDdoZwtDBX7gfpolkk6Bcmj6YFMrtCMVgoU/x2DI1p74DmYl7CSS6Ppa3xgemrLrr5IjIn0w==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -9274,6 +9340,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-freebsd-64/0.15.16:
|
||||||
|
resolution: {integrity: sha512-UzIc0xlRx5x9kRuMr+E3+hlSOxa/aRqfuMfiYBXu2jJ8Mzej4lGL7+o6F5hzhLqWfWm1GWHNakIdlqg1ayaTNQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/esbuild-freebsd-arm64/0.15.10:
|
/esbuild-freebsd-arm64/0.15.10:
|
||||||
resolution: {integrity: sha512-m2KNbuCX13yQqLlbSojFMHpewbn8wW5uDS6DxRpmaZKzyq8Dbsku6hHvh2U+BcLwWY4mpgXzFUoENEf7IcioGg==}
|
resolution: {integrity: sha512-m2KNbuCX13yQqLlbSojFMHpewbn8wW5uDS6DxRpmaZKzyq8Dbsku6hHvh2U+BcLwWY4mpgXzFUoENEf7IcioGg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -9283,6 +9358,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-freebsd-arm64/0.15.16:
|
||||||
|
resolution: {integrity: sha512-8xyiYuGc0DLZphFQIiYaLHlfoP+hAN9RHbE+Ibh8EUcDNHAqbQgUrQg7pE7Bo00rXmQ5Ap6KFgcR0b4ALZls1g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [freebsd]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/esbuild-linux-32/0.15.10:
|
/esbuild-linux-32/0.15.10:
|
||||||
resolution: {integrity: sha512-guXrwSYFAvNkuQ39FNeV4sNkNms1bLlA5vF1H0cazZBOLdLFIny6BhT+TUbK/hdByMQhtWQ5jI9VAmPKbVPu1w==}
|
resolution: {integrity: sha512-guXrwSYFAvNkuQ39FNeV4sNkNms1bLlA5vF1H0cazZBOLdLFIny6BhT+TUbK/hdByMQhtWQ5jI9VAmPKbVPu1w==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -9292,6 +9376,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-linux-32/0.15.16:
|
||||||
|
resolution: {integrity: sha512-iGijUTV+0kIMyUVoynK0v+32Oi8yyp0xwMzX69GX+5+AniNy/C/AL1MjFTsozRp/3xQPl7jVux/PLe2ds10/2w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/esbuild-linux-64/0.15.10:
|
/esbuild-linux-64/0.15.10:
|
||||||
resolution: {integrity: sha512-jd8XfaSJeucMpD63YNMO1JCrdJhckHWcMv6O233bL4l6ogQKQOxBYSRP/XLWP+6kVTu0obXovuckJDcA0DKtQA==}
|
resolution: {integrity: sha512-jd8XfaSJeucMpD63YNMO1JCrdJhckHWcMv6O233bL4l6ogQKQOxBYSRP/XLWP+6kVTu0obXovuckJDcA0DKtQA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -9301,6 +9394,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-linux-64/0.15.16:
|
||||||
|
resolution: {integrity: sha512-tuSOjXdLw7VzaUj89fIdAaQT7zFGbKBcz4YxbWrOiXkwscYgE7HtTxUavreBbnRkGxKwr9iT/gmeJWNm4djy/g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/esbuild-linux-arm/0.15.10:
|
/esbuild-linux-arm/0.15.10:
|
||||||
resolution: {integrity: sha512-6N8vThLL/Lysy9y4Ex8XoLQAlbZKUyExCWyayGi2KgTBelKpPgj6RZnUaKri0dHNPGgReJriKVU6+KDGQwn10A==}
|
resolution: {integrity: sha512-6N8vThLL/Lysy9y4Ex8XoLQAlbZKUyExCWyayGi2KgTBelKpPgj6RZnUaKri0dHNPGgReJriKVU6+KDGQwn10A==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -9310,6 +9412,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-linux-arm/0.15.16:
|
||||||
|
resolution: {integrity: sha512-XKcrxCEXDTOuoRj5l12tJnkvuxXBMKwEC5j0JISw3ziLf0j4zIwXbKbTmUrKFWbo6ZgvNpa7Y5dnbsjVvH39bQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/esbuild-linux-arm64/0.15.10:
|
/esbuild-linux-arm64/0.15.10:
|
||||||
resolution: {integrity: sha512-GByBi4fgkvZFTHFDYNftu1DQ1GzR23jws0oWyCfhnI7eMOe+wgwWrc78dbNk709Ivdr/evefm2PJiUBMiusS1A==}
|
resolution: {integrity: sha512-GByBi4fgkvZFTHFDYNftu1DQ1GzR23jws0oWyCfhnI7eMOe+wgwWrc78dbNk709Ivdr/evefm2PJiUBMiusS1A==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -9319,6 +9430,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-linux-arm64/0.15.16:
|
||||||
|
resolution: {integrity: sha512-mPYksnfHnemNrvjrDhZyixL/AfbJN0Xn9S34ZOHYdh6/jJcNd8iTsv3JwJoEvTJqjMggjMhGUPJAdjnFBHoH8A==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/esbuild-linux-mips64le/0.15.10:
|
/esbuild-linux-mips64le/0.15.10:
|
||||||
resolution: {integrity: sha512-BxP+LbaGVGIdQNJUNF7qpYjEGWb0YyHVSKqYKrn+pTwH/SiHUxFyJYSP3pqkku61olQiSBnSmWZ+YUpj78Tw7Q==}
|
resolution: {integrity: sha512-BxP+LbaGVGIdQNJUNF7qpYjEGWb0YyHVSKqYKrn+pTwH/SiHUxFyJYSP3pqkku61olQiSBnSmWZ+YUpj78Tw7Q==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -9328,6 +9448,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-linux-mips64le/0.15.16:
|
||||||
|
resolution: {integrity: sha512-kSJO2PXaxfm0pWY39+YX+QtpFqyyrcp0ZeI8QPTrcFVQoWEPiPVtOfTZeS3ZKedfH+Ga38c4DSzmKMQJocQv6A==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [mips64el]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/esbuild-linux-ppc64le/0.15.10:
|
/esbuild-linux-ppc64le/0.15.10:
|
||||||
resolution: {integrity: sha512-LoSQCd6498PmninNgqd/BR7z3Bsk/mabImBWuQ4wQgmQEeanzWd5BQU2aNi9mBURCLgyheuZS6Xhrw5luw3OkQ==}
|
resolution: {integrity: sha512-LoSQCd6498PmninNgqd/BR7z3Bsk/mabImBWuQ4wQgmQEeanzWd5BQU2aNi9mBURCLgyheuZS6Xhrw5luw3OkQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -9337,6 +9466,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-linux-ppc64le/0.15.16:
|
||||||
|
resolution: {integrity: sha512-NimPikwkBY0yGABw6SlhKrtT35sU4O23xkhlrTT/O6lSxv3Pm5iSc6OYaqVAHWkLdVf31bF4UDVFO+D990WpAA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/esbuild-linux-riscv64/0.15.10:
|
/esbuild-linux-riscv64/0.15.10:
|
||||||
resolution: {integrity: sha512-Lrl9Cr2YROvPV4wmZ1/g48httE8z/5SCiXIyebiB5N8VT7pX3t6meI7TQVHw/wQpqP/AF4SksDuFImPTM7Z32Q==}
|
resolution: {integrity: sha512-Lrl9Cr2YROvPV4wmZ1/g48httE8z/5SCiXIyebiB5N8VT7pX3t6meI7TQVHw/wQpqP/AF4SksDuFImPTM7Z32Q==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -9346,6 +9484,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-linux-riscv64/0.15.16:
|
||||||
|
resolution: {integrity: sha512-ty2YUHZlwFOwp7pR+J87M4CVrXJIf5ZZtU/umpxgVJBXvWjhziSLEQxvl30SYfUPq0nzeWKBGw5i/DieiHeKfw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/esbuild-linux-s390x/0.15.10:
|
/esbuild-linux-s390x/0.15.10:
|
||||||
resolution: {integrity: sha512-ReP+6q3eLVVP2lpRrvl5EodKX7EZ1bS1/z5j6hsluAlZP5aHhk6ghT6Cq3IANvvDdscMMCB4QEbI+AjtvoOFpA==}
|
resolution: {integrity: sha512-ReP+6q3eLVVP2lpRrvl5EodKX7EZ1bS1/z5j6hsluAlZP5aHhk6ghT6Cq3IANvvDdscMMCB4QEbI+AjtvoOFpA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -9355,6 +9502,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-linux-s390x/0.15.16:
|
||||||
|
resolution: {integrity: sha512-VkZaGssvPDQtx4fvVdZ9czezmyWyzpQhEbSNsHZZN0BHvxRLOYAQ7sjay8nMQwYswP6O2KlZluRMNPYefFRs+w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/esbuild-netbsd-64/0.15.10:
|
/esbuild-netbsd-64/0.15.10:
|
||||||
resolution: {integrity: sha512-iGDYtJCMCqldMskQ4eIV+QSS/CuT7xyy9i2/FjpKvxAuCzrESZXiA1L64YNj6/afuzfBe9i8m/uDkFHy257hTw==}
|
resolution: {integrity: sha512-iGDYtJCMCqldMskQ4eIV+QSS/CuT7xyy9i2/FjpKvxAuCzrESZXiA1L64YNj6/afuzfBe9i8m/uDkFHy257hTw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -9364,6 +9520,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-netbsd-64/0.15.16:
|
||||||
|
resolution: {integrity: sha512-ElQ9rhdY51et6MJTWrCPbqOd/YuPowD7Cxx3ee8wlmXQQVW7UvQI6nSprJ9uVFQISqSF5e5EWpwWqXZsECLvXg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [netbsd]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/esbuild-openbsd-64/0.15.10:
|
/esbuild-openbsd-64/0.15.10:
|
||||||
resolution: {integrity: sha512-ftMMIwHWrnrYnvuJQRJs/Smlcb28F9ICGde/P3FUTCgDDM0N7WA0o9uOR38f5Xe2/OhNCgkjNeb7QeaE3cyWkQ==}
|
resolution: {integrity: sha512-ftMMIwHWrnrYnvuJQRJs/Smlcb28F9ICGde/P3FUTCgDDM0N7WA0o9uOR38f5Xe2/OhNCgkjNeb7QeaE3cyWkQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -9373,6 +9538,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-openbsd-64/0.15.16:
|
||||||
|
resolution: {integrity: sha512-KgxMHyxMCT+NdLQE1zVJEsLSt2QQBAvJfmUGDmgEq8Fvjrf6vSKB00dVHUEDKcJwMID6CdgCpvYNt999tIYhqA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [openbsd]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/esbuild-sunos-64/0.15.10:
|
/esbuild-sunos-64/0.15.10:
|
||||||
resolution: {integrity: sha512-mf7hBL9Uo2gcy2r3rUFMjVpTaGpFJJE5QTDDqUFf1632FxteYANffDZmKbqX0PfeQ2XjUDE604IcE7OJeoHiyg==}
|
resolution: {integrity: sha512-mf7hBL9Uo2gcy2r3rUFMjVpTaGpFJJE5QTDDqUFf1632FxteYANffDZmKbqX0PfeQ2XjUDE604IcE7OJeoHiyg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -9382,6 +9556,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-sunos-64/0.15.16:
|
||||||
|
resolution: {integrity: sha512-exSAx8Phj7QylXHlMfIyEfNrmqnLxFqLxdQF6MBHPdHAjT7fsKaX6XIJn+aQEFiOcE4X8e7VvdMCJ+WDZxjSRQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [sunos]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/esbuild-windows-32/0.15.10:
|
/esbuild-windows-32/0.15.10:
|
||||||
resolution: {integrity: sha512-ttFVo+Cg8b5+qHmZHbEc8Vl17kCleHhLzgT8X04y8zudEApo0PxPg9Mz8Z2cKH1bCYlve1XL8LkyXGFjtUYeGg==}
|
resolution: {integrity: sha512-ttFVo+Cg8b5+qHmZHbEc8Vl17kCleHhLzgT8X04y8zudEApo0PxPg9Mz8Z2cKH1bCYlve1XL8LkyXGFjtUYeGg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -9391,6 +9574,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-windows-32/0.15.16:
|
||||||
|
resolution: {integrity: sha512-zQgWpY5pUCSTOwqKQ6/vOCJfRssTvxFuEkpB4f2VUGPBpdddZfdj8hbZuFRdZRPIVHvN7juGcpgCA/XCF37mAQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/esbuild-windows-64/0.15.10:
|
/esbuild-windows-64/0.15.10:
|
||||||
resolution: {integrity: sha512-2H0gdsyHi5x+8lbng3hLbxDWR7mKHWh5BXZGKVG830KUmXOOWFE2YKJ4tHRkejRduOGDrBvHBriYsGtmTv3ntA==}
|
resolution: {integrity: sha512-2H0gdsyHi5x+8lbng3hLbxDWR7mKHWh5BXZGKVG830KUmXOOWFE2YKJ4tHRkejRduOGDrBvHBriYsGtmTv3ntA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -9400,6 +9592,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-windows-64/0.15.16:
|
||||||
|
resolution: {integrity: sha512-HjW1hHRLSncnM3MBCP7iquatHVJq9l0S2xxsHHj4yzf4nm9TU4Z7k4NkeMlD/dHQ4jPlQQhwcMvwbJiOefSuZw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/esbuild-windows-arm64/0.15.10:
|
/esbuild-windows-arm64/0.15.10:
|
||||||
resolution: {integrity: sha512-S+th4F+F8VLsHLR0zrUcG+Et4hx0RKgK1eyHc08kztmLOES8BWwMiaGdoW9hiXuzznXQ0I/Fg904MNbr11Nktw==}
|
resolution: {integrity: sha512-S+th4F+F8VLsHLR0zrUcG+Et4hx0RKgK1eyHc08kztmLOES8BWwMiaGdoW9hiXuzznXQ0I/Fg904MNbr11Nktw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -9409,6 +9610,15 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/esbuild-windows-arm64/0.15.16:
|
||||||
|
resolution: {integrity: sha512-oCcUKrJaMn04Vxy9Ekd8x23O8LoU01+4NOkQ2iBToKgnGj5eo1vU9i27NQZ9qC8NFZgnQQZg5oZWAejmbsppNA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/esbuild/0.15.10:
|
/esbuild/0.15.10:
|
||||||
resolution: {integrity: sha512-N7wBhfJ/E5fzn/SpNgX+oW2RLRjwaL8Y0ezqNqhjD6w0H2p0rDuEz2FKZqpqLnO8DCaWumKe8dsC/ljvVSSxng==}
|
resolution: {integrity: sha512-N7wBhfJ/E5fzn/SpNgX+oW2RLRjwaL8Y0ezqNqhjD6w0H2p0rDuEz2FKZqpqLnO8DCaWumKe8dsC/ljvVSSxng==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -9439,6 +9649,36 @@ packages:
|
|||||||
esbuild-windows-arm64: 0.15.10
|
esbuild-windows-arm64: 0.15.10
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/esbuild/0.15.16:
|
||||||
|
resolution: {integrity: sha512-o6iS9zxdHrrojjlj6pNGC2NAg86ECZqIETswTM5KmJitq+R1YmahhWtMumeQp9lHqJaROGnsBi2RLawGnfo5ZQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
hasBin: true
|
||||||
|
requiresBuild: true
|
||||||
|
optionalDependencies:
|
||||||
|
'@esbuild/android-arm': 0.15.16
|
||||||
|
'@esbuild/linux-loong64': 0.15.16
|
||||||
|
esbuild-android-64: 0.15.16
|
||||||
|
esbuild-android-arm64: 0.15.16
|
||||||
|
esbuild-darwin-64: 0.15.16
|
||||||
|
esbuild-darwin-arm64: 0.15.16
|
||||||
|
esbuild-freebsd-64: 0.15.16
|
||||||
|
esbuild-freebsd-arm64: 0.15.16
|
||||||
|
esbuild-linux-32: 0.15.16
|
||||||
|
esbuild-linux-64: 0.15.16
|
||||||
|
esbuild-linux-arm: 0.15.16
|
||||||
|
esbuild-linux-arm64: 0.15.16
|
||||||
|
esbuild-linux-mips64le: 0.15.16
|
||||||
|
esbuild-linux-ppc64le: 0.15.16
|
||||||
|
esbuild-linux-riscv64: 0.15.16
|
||||||
|
esbuild-linux-s390x: 0.15.16
|
||||||
|
esbuild-netbsd-64: 0.15.16
|
||||||
|
esbuild-openbsd-64: 0.15.16
|
||||||
|
esbuild-sunos-64: 0.15.16
|
||||||
|
esbuild-windows-32: 0.15.16
|
||||||
|
esbuild-windows-64: 0.15.16
|
||||||
|
esbuild-windows-arm64: 0.15.16
|
||||||
|
dev: true
|
||||||
|
|
||||||
/escalade/3.1.1:
|
/escalade/3.1.1:
|
||||||
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
|
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -17380,11 +17620,11 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
bundle-require: 3.1.2_esbuild@0.15.10
|
bundle-require: 3.1.2_esbuild@0.15.16
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
chokidar: 3.5.3
|
chokidar: 3.5.3
|
||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
esbuild: 0.15.10
|
esbuild: 0.15.16
|
||||||
execa: 5.1.1
|
execa: 5.1.1
|
||||||
globby: 11.1.0
|
globby: 11.1.0
|
||||||
joycon: 3.1.1
|
joycon: 3.1.1
|
||||||
|
Reference in New Issue
Block a user