feat(editor): ✨ Team workspaces
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import { CollaborationType, Prisma, User } from 'db'
|
||||
import { CollaborationType, Prisma, User, WorkspaceRole } from 'db'
|
||||
|
||||
const parseWhereFilter = (
|
||||
typebotIds: string[] | string,
|
||||
@ -6,14 +6,6 @@ const parseWhereFilter = (
|
||||
type: 'read' | 'write'
|
||||
): Prisma.TypebotWhereInput => ({
|
||||
OR: [
|
||||
{
|
||||
id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds },
|
||||
ownerId:
|
||||
(type === 'read' && user.email === process.env.ADMIN_EMAIL) ||
|
||||
process.env.NEXT_PUBLIC_E2E_TEST
|
||||
? undefined
|
||||
: user.id,
|
||||
},
|
||||
{
|
||||
id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds },
|
||||
collaborators: {
|
||||
@ -23,6 +15,18 @@ const parseWhereFilter = (
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: typeof typebotIds === 'string' ? typebotIds : { in: typebotIds },
|
||||
workspace:
|
||||
(type === 'read' && user.email === process.env.ADMIN_EMAIL) ||
|
||||
process.env.NEXT_PUBLIC_E2E_TEST
|
||||
? undefined
|
||||
: {
|
||||
members: {
|
||||
some: { userId: user.id, role: { not: WorkspaceRole.GUEST } },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
@ -37,3 +41,12 @@ export const canReadTypebots = (typebotIds: string[], user: User) =>
|
||||
|
||||
export const canWriteTypebots = (typebotIds: string[], user: User) =>
|
||||
parseWhereFilter(typebotIds, user, 'write')
|
||||
|
||||
export const canEditGuests = (user: User, typebotId: string) => ({
|
||||
id: typebotId,
|
||||
workspace: {
|
||||
members: {
|
||||
some: { userId: user.id, role: { not: WorkspaceRole.GUEST } },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
@ -1,17 +1,18 @@
|
||||
import { Credentials } from 'models'
|
||||
import { stringify } from 'qs'
|
||||
import useSWR from 'swr'
|
||||
import { sendRequest } from 'utils'
|
||||
import { fetcher } from '../utils'
|
||||
import { fetcher } from './utils'
|
||||
|
||||
export const useCredentials = ({
|
||||
userId,
|
||||
workspaceId,
|
||||
onError,
|
||||
}: {
|
||||
userId?: string
|
||||
workspaceId?: string
|
||||
onError?: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<{ credentials: Credentials[] }, Error>(
|
||||
userId ? `/api/users/${userId}/credentials` : null,
|
||||
workspaceId ? `/api/credentials?${stringify({ workspaceId })}` : null,
|
||||
fetcher
|
||||
)
|
||||
if (error && onError) onError(error)
|
||||
@ -23,24 +24,25 @@ export const useCredentials = ({
|
||||
}
|
||||
|
||||
export const createCredentials = async (
|
||||
userId: string,
|
||||
credentials: Omit<Credentials, 'ownerId' | 'id' | 'iv' | 'createdAt'>
|
||||
credentials: Omit<Credentials, 'id' | 'iv' | 'createdAt' | 'ownerId'>
|
||||
) =>
|
||||
sendRequest<{
|
||||
credentials: Credentials
|
||||
}>({
|
||||
url: `/api/users/${userId}/credentials`,
|
||||
url: `/api/credentials?${stringify({
|
||||
workspaceId: credentials.workspaceId,
|
||||
})}`,
|
||||
method: 'POST',
|
||||
body: credentials,
|
||||
})
|
||||
|
||||
export const deleteCredentials = async (
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
credentialsId: string
|
||||
) =>
|
||||
sendRequest<{
|
||||
credentials: Credentials
|
||||
}>({
|
||||
url: `/api/users/${userId}/credentials/${credentialsId}`,
|
||||
url: `/api/credentials/${credentialsId}?${stringify({ workspaceId })}`,
|
||||
method: 'DELETE',
|
||||
})
|
@ -1,20 +1,24 @@
|
||||
import { CustomDomain } from 'db'
|
||||
import { Credentials } from 'models'
|
||||
import { stringify } from 'qs'
|
||||
import useSWR from 'swr'
|
||||
import { sendRequest } from 'utils'
|
||||
import { fetcher } from '../utils'
|
||||
import { fetcher } from './utils'
|
||||
|
||||
export const useCustomDomains = ({
|
||||
userId,
|
||||
workspaceId,
|
||||
onError,
|
||||
}: {
|
||||
userId?: string
|
||||
workspaceId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<
|
||||
{ customDomains: Omit<CustomDomain, 'createdAt'>[] },
|
||||
{ customDomains: Omit<CustomDomain, 'createdAt' | 'ownerId'>[] },
|
||||
Error
|
||||
>(userId ? `/api/users/${userId}/customDomains` : null, fetcher)
|
||||
>(
|
||||
workspaceId ? `/api/customDomains?${stringify({ workspaceId })}` : null,
|
||||
fetcher
|
||||
)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
customDomains: data?.customDomains,
|
||||
@ -24,24 +28,24 @@ export const useCustomDomains = ({
|
||||
}
|
||||
|
||||
export const createCustomDomain = async (
|
||||
userId: string,
|
||||
customDomain: Omit<CustomDomain, 'ownerId' | 'createdAt'>
|
||||
workspaceId: string,
|
||||
customDomain: Omit<CustomDomain, 'createdAt' | 'workspaceId' | 'ownerId'>
|
||||
) =>
|
||||
sendRequest<{
|
||||
credentials: Credentials
|
||||
}>({
|
||||
url: `/api/users/${userId}/customDomains`,
|
||||
url: `/api/customDomains?${stringify({ workspaceId })}`,
|
||||
method: 'POST',
|
||||
body: customDomain,
|
||||
})
|
||||
|
||||
export const deleteCustomDomain = async (
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
customDomain: string
|
||||
) =>
|
||||
sendRequest<{
|
||||
credentials: Credentials
|
||||
}>({
|
||||
url: `/api/users/${userId}/customDomains/${customDomain}`,
|
||||
url: `/api/customDomains/${customDomain}?${stringify({ workspaceId })}`,
|
||||
method: 'DELETE',
|
||||
})
|
@ -6,14 +6,16 @@ import { sendRequest } from 'utils'
|
||||
|
||||
export const useFolders = ({
|
||||
parentId,
|
||||
workspaceId,
|
||||
onError,
|
||||
}: {
|
||||
workspaceId?: string
|
||||
parentId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const params = stringify({ parentId })
|
||||
const params = stringify({ parentId, workspaceId })
|
||||
const { data, error, mutate } = useSWR<{ folders: DashboardFolder[] }, Error>(
|
||||
`/api/folders?${params}`,
|
||||
workspaceId ? `/api/folders?${params}` : null,
|
||||
fetcher,
|
||||
{ dedupingInterval: process.env.NEXT_PUBLIC_E2E_TEST ? 0 : undefined }
|
||||
)
|
||||
@ -45,12 +47,13 @@ export const useFolderContent = ({
|
||||
}
|
||||
|
||||
export const createFolder = async (
|
||||
workspaceId: string,
|
||||
folder: Pick<DashboardFolder, 'parentFolderId'>
|
||||
) =>
|
||||
sendRequest<DashboardFolder>({
|
||||
url: `/api/folders`,
|
||||
method: 'POST',
|
||||
body: folder,
|
||||
body: { ...folder, workspaceId },
|
||||
})
|
||||
|
||||
export const deleteFolder = async (id: string) =>
|
||||
|
@ -11,9 +11,10 @@ import {
|
||||
|
||||
export const getGoogleSheetsConsentScreenUrl = (
|
||||
redirectUrl: string,
|
||||
stepId: string
|
||||
stepId: string,
|
||||
workspaceId?: string
|
||||
) => {
|
||||
const queryParams = stringify({ redirectUrl, stepId })
|
||||
const queryParams = stringify({ redirectUrl, stepId, workspaceId })
|
||||
return `/api/credentials/google-sheets/consent-url?${queryParams}`
|
||||
}
|
||||
|
||||
|
@ -38,6 +38,7 @@ export const parsePublicTypebotToTypebot = (
|
||||
folderId: existingTypebot.folderId,
|
||||
ownerId: existingTypebot.ownerId,
|
||||
icon: existingTypebot.icon,
|
||||
workspaceId: existingTypebot.workspaceId,
|
||||
})
|
||||
|
||||
export const createPublishedTypebot = async (typebot: PublicTypebot) =>
|
||||
|
@ -2,14 +2,21 @@ import { User } from 'db'
|
||||
import { loadStripe } from '@stripe/stripe-js'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const pay = async (user: User, currency: 'usd' | 'eur') => {
|
||||
type Props = {
|
||||
user: User
|
||||
currency: 'usd' | 'eur'
|
||||
plan: 'pro' | 'team'
|
||||
workspaceId: string
|
||||
}
|
||||
|
||||
export const pay = async ({ user, currency, plan, workspaceId }: Props) => {
|
||||
if (!process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY)
|
||||
throw new Error('NEXT_PUBLIC_STRIPE_PUBLIC_KEY is missing in env')
|
||||
const stripe = await loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY)
|
||||
const { data, error } = await sendRequest<{ sessionId: string }>({
|
||||
method: 'POST',
|
||||
url: '/api/stripe/checkout',
|
||||
body: { email: user.email, currency },
|
||||
body: { email: user.email, currency, plan, workspaceId },
|
||||
})
|
||||
if (error || !data) return
|
||||
return stripe?.redirectToCheckout({
|
||||
|
@ -36,7 +36,7 @@ export const updateCollaborator = (
|
||||
collaborator: CollaboratorsOnTypebots
|
||||
) =>
|
||||
sendRequest({
|
||||
method: 'PUT',
|
||||
method: 'PATCH',
|
||||
url: `/api/typebots/${typebotId}/collaborators/${userId}`,
|
||||
body: collaborator,
|
||||
})
|
||||
|
@ -36,10 +36,10 @@ export const sendInvitation = (
|
||||
export const updateInvitation = (
|
||||
typebotId: string,
|
||||
email: string,
|
||||
invitation: Omit<Invitation, 'createdAt'>
|
||||
invitation: Omit<Invitation, 'createdAt' | 'id'>
|
||||
) =>
|
||||
sendRequest({
|
||||
method: 'PUT',
|
||||
method: 'PATCH',
|
||||
url: `/api/typebots/${typebotId}/invitations/${email}`,
|
||||
body: invitation,
|
||||
})
|
||||
|
@ -64,18 +64,20 @@ export type TypebotInDashboard = Pick<
|
||||
>
|
||||
export const useTypebots = ({
|
||||
folderId,
|
||||
workspaceId,
|
||||
allFolders,
|
||||
onError,
|
||||
}: {
|
||||
workspaceId?: string
|
||||
folderId?: string
|
||||
allFolders?: boolean
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const params = stringify({ folderId, allFolders })
|
||||
const params = stringify({ folderId, allFolders, workspaceId })
|
||||
const { data, error, mutate } = useSWR<
|
||||
{ typebots: TypebotInDashboard[] },
|
||||
Error
|
||||
>(`/api/typebots?${params}`, fetcher, {
|
||||
>(workspaceId ? `/api/typebots?${params}` : null, fetcher, {
|
||||
dedupingInterval: process.env.NEXT_PUBLIC_E2E_TEST ? 0 : undefined,
|
||||
})
|
||||
if (error) onError(error)
|
||||
@ -88,10 +90,12 @@ export const useTypebots = ({
|
||||
|
||||
export const createTypebot = async ({
|
||||
folderId,
|
||||
}: Pick<Typebot, 'folderId'>) => {
|
||||
workspaceId,
|
||||
}: Pick<Typebot, 'folderId' | 'workspaceId'>) => {
|
||||
const typebot = {
|
||||
folderId,
|
||||
name: 'My typebot',
|
||||
workspaceId,
|
||||
}
|
||||
return sendRequest<Typebot>({
|
||||
url: `/api/typebots`,
|
||||
@ -379,13 +383,13 @@ export const parseDefaultPublicId = (name: string, id: string) =>
|
||||
toKebabCase(name) + `-${id?.slice(-7)}`
|
||||
|
||||
export const parseNewTypebot = ({
|
||||
ownerId,
|
||||
folderId,
|
||||
name,
|
||||
ownerAvatarUrl,
|
||||
workspaceId,
|
||||
}: {
|
||||
ownerId: string
|
||||
folderId: string | null
|
||||
workspaceId: string
|
||||
name: string
|
||||
ownerAvatarUrl?: string
|
||||
}): Omit<
|
||||
@ -413,9 +417,10 @@ export const parseNewTypebot = ({
|
||||
steps: [startStep],
|
||||
}
|
||||
return {
|
||||
ownerId: null,
|
||||
folderId,
|
||||
name,
|
||||
ownerId,
|
||||
workspaceId,
|
||||
blocks: [startBlock],
|
||||
edges: [],
|
||||
variables: [],
|
||||
|
@ -1,3 +1,3 @@
|
||||
export * from './user'
|
||||
export * from './customDomains'
|
||||
export * from './credentials'
|
||||
export * from '../customDomains'
|
||||
export * from '../credentials'
|
||||
|
@ -1,47 +0,0 @@
|
||||
import { Typebot } from 'models'
|
||||
import { fetcher } from 'services/utils'
|
||||
import useSWR from 'swr'
|
||||
import { isNotDefined } from 'utils'
|
||||
|
||||
export const useSharedTypebotsCount = ({
|
||||
userId,
|
||||
onError,
|
||||
}: {
|
||||
userId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<{ count: number }, Error>(
|
||||
userId ? `/api/users/${userId}/sharedTypebots?count=true` : null,
|
||||
fetcher
|
||||
)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
totalSharedTypebots: data?.count ?? 0,
|
||||
isLoading: !error && isNotDefined(data?.count),
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
export const useSharedTypebots = ({
|
||||
userId,
|
||||
onError,
|
||||
}: {
|
||||
userId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<
|
||||
{
|
||||
sharedTypebots: Pick<
|
||||
Typebot,
|
||||
'name' | 'id' | 'publishedTypebotId' | 'icon'
|
||||
>[]
|
||||
},
|
||||
Error
|
||||
>(userId ? `/api/users/${userId}/sharedTypebots` : null, fetcher)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
sharedTypebots: data?.sharedTypebots,
|
||||
isLoading: !error && isNotDefined(data),
|
||||
mutate,
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { Plan, User } from 'db'
|
||||
import { isNotDefined, sendRequest } from 'utils'
|
||||
import { User } from 'db'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const updateUser = async (id: string, user: User) =>
|
||||
sendRequest({
|
||||
@ -7,6 +7,3 @@ export const updateUser = async (id: string, user: User) =>
|
||||
method: 'PUT',
|
||||
body: user,
|
||||
})
|
||||
|
||||
export const isFreePlan = (user?: User) =>
|
||||
isNotDefined(user) || user?.plan === Plan.FREE
|
||||
|
3
apps/builder/services/workspace/index.ts
Normal file
3
apps/builder/services/workspace/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './workspace'
|
||||
export * from './member'
|
||||
export * from './invitation'
|
28
apps/builder/services/workspace/invitation.ts
Normal file
28
apps/builder/services/workspace/invitation.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { WorkspaceInvitation } from 'db'
|
||||
import { sendRequest } from 'utils'
|
||||
import { Member } from './member'
|
||||
|
||||
export const sendInvitation = (
|
||||
invitation: Omit<WorkspaceInvitation, 'id' | 'createdAt'>
|
||||
) =>
|
||||
sendRequest<{ invitation?: WorkspaceInvitation; member?: Member }>({
|
||||
url: `/api/workspaces/${invitation.workspaceId}/invitations`,
|
||||
method: 'POST',
|
||||
body: invitation,
|
||||
})
|
||||
|
||||
export const updateInvitation = (invitation: Partial<WorkspaceInvitation>) =>
|
||||
sendRequest({
|
||||
url: `/api/workspaces/${invitation.workspaceId}/invitations/${invitation.id}`,
|
||||
method: 'PATCH',
|
||||
body: invitation,
|
||||
})
|
||||
|
||||
export const deleteInvitation = (invitation: {
|
||||
workspaceId: string
|
||||
id: string
|
||||
}) =>
|
||||
sendRequest({
|
||||
url: `/api/workspaces/${invitation.workspaceId}/invitations/${invitation.id}`,
|
||||
method: 'DELETE',
|
||||
})
|
41
apps/builder/services/workspace/member.ts
Normal file
41
apps/builder/services/workspace/member.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { MemberInWorkspace, WorkspaceInvitation } from 'db'
|
||||
import { fetcher } from 'services/utils'
|
||||
import useSWR from 'swr'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export type Member = MemberInWorkspace & {
|
||||
name: string | null
|
||||
image: string | null
|
||||
email: string | null
|
||||
}
|
||||
|
||||
export const useMembers = ({ workspaceId }: { workspaceId?: string }) => {
|
||||
const { data, error, mutate } = useSWR<
|
||||
{ members: Member[]; invitations: WorkspaceInvitation[] },
|
||||
Error
|
||||
>(workspaceId ? `/api/workspaces/${workspaceId}/members` : null, fetcher, {
|
||||
dedupingInterval: process.env.NEXT_PUBLIC_E2E_TEST ? 0 : undefined,
|
||||
})
|
||||
return {
|
||||
members: data?.members,
|
||||
invitations: data?.invitations,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
export const updateMember = (
|
||||
workspaceId: string,
|
||||
member: Partial<MemberInWorkspace>
|
||||
) =>
|
||||
sendRequest({
|
||||
method: 'PATCH',
|
||||
url: `/api/workspaces/${workspaceId}/members/${member.userId}`,
|
||||
body: member,
|
||||
})
|
||||
|
||||
export const deleteMember = (workspaceId: string, userId: string) =>
|
||||
sendRequest({
|
||||
method: 'DELETE',
|
||||
url: `/api/workspaces/${workspaceId}/members/${userId}`,
|
||||
})
|
58
apps/builder/services/workspace/workspace.ts
Normal file
58
apps/builder/services/workspace/workspace.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { WorkspaceWithMembers } from 'contexts/WorkspaceContext'
|
||||
import { Plan, Workspace } from 'db'
|
||||
import useSWR from 'swr'
|
||||
import { isNotDefined, sendRequest } from 'utils'
|
||||
import { fetcher } from '../utils'
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
export const createNewWorkspace = async (
|
||||
body: Omit<Workspace, 'id' | 'icon' | 'createdAt' | 'stripeId'>
|
||||
) =>
|
||||
sendRequest<{
|
||||
workspace: Workspace
|
||||
}>({
|
||||
url: `/api/workspaces`,
|
||||
method: 'POST',
|
||||
body,
|
||||
})
|
||||
|
||||
export const updateWorkspace = async (updates: Partial<Workspace>) =>
|
||||
sendRequest<{
|
||||
workspace: Workspace
|
||||
}>({
|
||||
url: `/api/workspaces/${updates.id}`,
|
||||
method: 'PATCH',
|
||||
body: updates,
|
||||
})
|
||||
|
||||
export const planToReadable = (plan?: Plan) => {
|
||||
if (!plan) return
|
||||
switch (plan) {
|
||||
case Plan.FREE:
|
||||
return 'Free'
|
||||
case Plan.LIFETIME:
|
||||
return 'Lifetime'
|
||||
case Plan.OFFERED:
|
||||
return 'Offered'
|
||||
case Plan.PRO:
|
||||
return 'Pro'
|
||||
case Plan.TEAM:
|
||||
return 'Team'
|
||||
}
|
||||
}
|
||||
|
||||
export const isFreePlan = (workspace?: Pick<Workspace, 'plan'>) =>
|
||||
isNotDefined(workspace) || workspace?.plan === Plan.FREE
|
Reference in New Issue
Block a user