2
0

feat(editor): Team workspaces

This commit is contained in:
Baptiste Arnaud
2022-05-13 15:22:44 -07:00
parent 6c2986590b
commit f0fdf08b00
132 changed files with 3354 additions and 1228 deletions

View File

@ -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 } },
},
},
})

View File

@ -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',
})

View File

@ -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',
})

View File

@ -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) =>

View File

@ -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}`
}

View File

@ -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) =>

View File

@ -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({

View File

@ -36,7 +36,7 @@ export const updateCollaborator = (
collaborator: CollaboratorsOnTypebots
) =>
sendRequest({
method: 'PUT',
method: 'PATCH',
url: `/api/typebots/${typebotId}/collaborators/${userId}`,
body: collaborator,
})

View File

@ -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,
})

View File

@ -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: [],

View File

@ -1,3 +1,3 @@
export * from './user'
export * from './customDomains'
export * from './credentials'
export * from '../customDomains'
export * from '../credentials'

View File

@ -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,
}
}

View File

@ -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

View File

@ -0,0 +1,3 @@
export * from './workspace'
export * from './member'
export * from './invitation'

View 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',
})

View 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}`,
})

View 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