feat(editor): ✨ Team workspaces
This commit is contained in:
@ -40,7 +40,6 @@ import useUndo from 'services/utils/useUndo'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { itemsAction, ItemsActions } from './actions/items'
|
||||
import { dequal } from 'dequal'
|
||||
import { User } from 'db'
|
||||
import { saveWebhook } from 'services/webhook'
|
||||
import { stringify } from 'qs'
|
||||
import cuid from 'cuid'
|
||||
@ -63,7 +62,6 @@ const typebotContext = createContext<
|
||||
typebot?: Typebot
|
||||
publishedTypebot?: PublicTypebot
|
||||
linkedTypebots?: Typebot[]
|
||||
owner?: User
|
||||
webhooks: Webhook[]
|
||||
isReadOnly?: boolean
|
||||
isPublished: boolean
|
||||
@ -108,22 +106,15 @@ export const TypebotContext = ({
|
||||
status: 'error',
|
||||
})
|
||||
|
||||
const {
|
||||
typebot,
|
||||
publishedTypebot,
|
||||
owner,
|
||||
webhooks,
|
||||
isReadOnly,
|
||||
isLoading,
|
||||
mutate,
|
||||
} = useFetchedTypebot({
|
||||
typebotId,
|
||||
onError: (error) =>
|
||||
toast({
|
||||
title: 'Error while fetching typebot',
|
||||
description: error.message,
|
||||
}),
|
||||
})
|
||||
const { typebot, publishedTypebot, webhooks, isReadOnly, isLoading, mutate } =
|
||||
useFetchedTypebot({
|
||||
typebotId,
|
||||
onError: (error) =>
|
||||
toast({
|
||||
title: 'Error while fetching typebot',
|
||||
description: error.message,
|
||||
}),
|
||||
})
|
||||
|
||||
const [
|
||||
{ present: localTypebot },
|
||||
@ -150,6 +141,7 @@ export const TypebotContext = ({
|
||||
)
|
||||
|
||||
const { typebots: linkedTypebots } = useLinkedTypebots({
|
||||
workspaceId: localTypebot?.workspaceId ?? undefined,
|
||||
typebotId,
|
||||
typebotIds: linkedTypebotIds,
|
||||
onError: (error) =>
|
||||
@ -373,7 +365,6 @@ export const TypebotContext = ({
|
||||
typebot: localTypebot,
|
||||
publishedTypebot,
|
||||
linkedTypebots,
|
||||
owner,
|
||||
webhooks: webhooks ?? [],
|
||||
isReadOnly,
|
||||
isSavingLoading,
|
||||
@ -415,17 +406,17 @@ export const useFetchedTypebot = ({
|
||||
typebot: Typebot
|
||||
webhooks: Webhook[]
|
||||
publishedTypebot?: PublicTypebot
|
||||
owner?: User
|
||||
isReadOnly?: boolean
|
||||
},
|
||||
Error
|
||||
>(`/api/typebots/${typebotId}`, fetcher, { dedupingInterval: 0 })
|
||||
>(`/api/typebots/${typebotId}`, fetcher, {
|
||||
dedupingInterval: process.env.NEXT_PUBLIC_E2E_TEST ? 0 : undefined,
|
||||
})
|
||||
if (error) onError(error)
|
||||
return {
|
||||
typebot: data?.typebot,
|
||||
webhooks: data?.webhooks,
|
||||
publishedTypebot: data?.publishedTypebot,
|
||||
owner: data?.owner,
|
||||
isReadOnly: data?.isReadOnly,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
@ -433,15 +424,17 @@ export const useFetchedTypebot = ({
|
||||
}
|
||||
|
||||
const useLinkedTypebots = ({
|
||||
workspaceId,
|
||||
typebotId,
|
||||
typebotIds,
|
||||
onError,
|
||||
}: {
|
||||
workspaceId?: string
|
||||
typebotId?: string
|
||||
typebotIds?: string[]
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const params = stringify({ typebotIds }, { indices: false })
|
||||
const params = stringify({ typebotIds, workspaceId }, { indices: false })
|
||||
const { data, error, mutate } = useSWR<
|
||||
{
|
||||
typebots: Typebot[]
|
||||
|
@ -21,6 +21,7 @@ const userContext = createContext<{
|
||||
isSaving: boolean
|
||||
hasUnsavedChanges: boolean
|
||||
isOAuthProvider: boolean
|
||||
currentWorkspaceId?: string
|
||||
updateUser: (newUser: Partial<User>) => void
|
||||
saveUser: (newUser?: Partial<User>) => Promise<void>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
@ -35,6 +36,7 @@ export const UserContext = ({ children }: { children: ReactNode }) => {
|
||||
position: 'top-right',
|
||||
status: 'error',
|
||||
})
|
||||
const [currentWorkspaceId, setCurrentWorkspaceId] = useState<string>()
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const isOAuthProvider = useMemo(
|
||||
@ -49,6 +51,9 @@ export const UserContext = ({ children }: { children: ReactNode }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefined(user) || isNotDefined(session)) return
|
||||
setCurrentWorkspaceId(
|
||||
localStorage.getItem('currentWorkspaceId') ?? undefined
|
||||
)
|
||||
const parsedUser = session.user as User
|
||||
setUser(parsedUser)
|
||||
if (parsedUser?.id) setSentryUser({ id: parsedUser.id })
|
||||
@ -96,6 +101,7 @@ export const UserContext = ({ children }: { children: ReactNode }) => {
|
||||
isLoading: status === 'loading',
|
||||
hasUnsavedChanges,
|
||||
isOAuthProvider,
|
||||
currentWorkspaceId,
|
||||
updateUser,
|
||||
saveUser,
|
||||
}}
|
||||
|
144
apps/builder/contexts/WorkspaceContext.tsx
Normal file
144
apps/builder/contexts/WorkspaceContext.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { byId } from 'utils'
|
||||
import { MemberInWorkspace, Plan, Workspace, WorkspaceRole } from 'db'
|
||||
import {
|
||||
createNewWorkspace,
|
||||
useWorkspaces,
|
||||
updateWorkspace as patchWorkspace,
|
||||
} from 'services/workspace/workspace'
|
||||
import { useUser } from './UserContext'
|
||||
import { useTypebot } from './TypebotContext'
|
||||
|
||||
export type WorkspaceWithMembers = Workspace & { members: MemberInWorkspace[] }
|
||||
|
||||
const workspaceContext = createContext<{
|
||||
workspaces?: WorkspaceWithMembers[]
|
||||
isLoading: boolean
|
||||
workspace?: WorkspaceWithMembers
|
||||
canEdit: boolean
|
||||
currentRole?: WorkspaceRole
|
||||
switchWorkspace: (workspaceId: string) => void
|
||||
createWorkspace: (name?: string) => Promise<void>
|
||||
updateWorkspace: (
|
||||
workspaceId: string,
|
||||
updates: Partial<Workspace>
|
||||
) => Promise<void>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
}>({})
|
||||
|
||||
export const WorkspaceContext = ({ children }: { children: ReactNode }) => {
|
||||
const { user } = useUser()
|
||||
const userId = user?.id
|
||||
const { typebot } = useTypebot()
|
||||
const { workspaces, isLoading, mutate } = useWorkspaces({ userId })
|
||||
const [currentWorkspace, setCurrentWorkspace] =
|
||||
useState<WorkspaceWithMembers>()
|
||||
|
||||
const canEdit =
|
||||
workspaces
|
||||
?.find(byId(currentWorkspace?.id))
|
||||
?.members.find((m) => m.userId === userId)?.role === WorkspaceRole.ADMIN
|
||||
|
||||
const currentRole = currentWorkspace?.members.find(
|
||||
(m) => m.userId === userId
|
||||
)?.role
|
||||
|
||||
console.log(workspaces)
|
||||
useEffect(() => {
|
||||
if (!workspaces || workspaces.length === 0) return
|
||||
const lastWorspaceId = 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(() => {
|
||||
if (!currentWorkspace?.id) return
|
||||
localStorage.setItem('workspaceId', currentWorkspace.id)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentWorkspace?.id])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!typebot?.workspaceId ||
|
||||
!currentWorkspace ||
|
||||
typebot.workspaceId === currentWorkspace.id
|
||||
)
|
||||
return
|
||||
switchWorkspace(typebot.workspaceId)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [typebot?.workspaceId])
|
||||
|
||||
const switchWorkspace = (workspaceId: string) =>
|
||||
setCurrentWorkspace(workspaces?.find(byId(workspaceId)))
|
||||
|
||||
const createWorkspace = async (name?: string) => {
|
||||
if (!workspaces) return
|
||||
const { data, error } = await createNewWorkspace({
|
||||
name: name ? `${name}'s workspace` : 'My workspace',
|
||||
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 (
|
||||
workspaceId: string,
|
||||
updates: Partial<Workspace>
|
||||
) => {
|
||||
const { data } = await patchWorkspace({ id: workspaceId, ...updates })
|
||||
if (!data || !currentWorkspace) return
|
||||
setCurrentWorkspace({ ...currentWorkspace, ...updates })
|
||||
mutate({
|
||||
workspaces: (workspaces ?? []).map((w) =>
|
||||
w.id === workspaceId ? { ...data.workspace, members: w.members } : w
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<workspaceContext.Provider
|
||||
value={{
|
||||
workspaces,
|
||||
workspace: currentWorkspace,
|
||||
isLoading,
|
||||
canEdit,
|
||||
currentRole,
|
||||
switchWorkspace,
|
||||
createWorkspace,
|
||||
updateWorkspace,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</workspaceContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useWorkspace = () => useContext(workspaceContext)
|
Reference in New Issue
Block a user