feat(kb): added permissions to workspace popover, added kb popover to view tags, edit description and kb name (#2634)

This commit is contained in:
Waleed
2025-12-29 18:56:50 -08:00
committed by GitHub
parent e9e5721610
commit 97a9295230
7 changed files with 672 additions and 58 deletions

View File

@@ -1,8 +1,14 @@
'use client'
import Link from 'next/link'
import { useParams } from 'next/navigation'
import { useCallback, useState } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { Badge, DocumentAttachment, Tooltip } from '@/components/emcn'
import { BaseTagsModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/components'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
import { DeleteKnowledgeBaseModal } from '../delete-knowledge-base-modal/delete-knowledge-base-modal'
import { EditKnowledgeBaseModal } from '../edit-knowledge-base-modal/edit-knowledge-base-modal'
import { KnowledgeBaseContextMenu } from '../knowledge-base-context-menu/knowledge-base-context-menu'
interface BaseCardProps {
id?: string
@@ -11,6 +17,8 @@ interface BaseCardProps {
description: string
createdAt?: string
updatedAt?: string
onUpdate?: (id: string, name: string, description: string) => Promise<void>
onDelete?: (id: string) => Promise<void>
}
/**
@@ -109,9 +117,32 @@ export function BaseCardSkeletonGrid({ count = 8 }: { count?: number }) {
/**
* Knowledge base card component displaying overview information
*/
export function BaseCard({ id, title, docCount, description, updatedAt }: BaseCardProps) {
export function BaseCard({
id,
title,
docCount,
description,
updatedAt,
onUpdate,
onDelete,
}: BaseCardProps) {
const params = useParams()
const router = useRouter()
const workspaceId = params?.workspaceId as string
const userPermissions = useUserPermissionsContext()
const {
isOpen: isContextMenuOpen,
position: contextMenuPosition,
menuRef,
handleContextMenu,
closeMenu: closeContextMenu,
} = useContextMenu()
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const [isTagsModalOpen, setIsTagsModalOpen] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const searchParams = new URLSearchParams({
kbName: title,
@@ -120,41 +151,154 @@ export function BaseCard({ id, title, docCount, description, updatedAt }: BaseCa
const shortId = id ? `kb-${id.slice(0, 8)}` : ''
return (
<Link href={href} prefetch={true} className='h-full'>
<div className='group flex h-full cursor-pointer flex-col gap-[12px] rounded-[4px] bg-[var(--surface-3)] px-[8px] py-[6px] transition-colors hover:bg-[var(--surface-4)] dark:bg-[var(--surface-4)] dark:hover:bg-[var(--surface-5)]'>
<div className='flex items-center justify-between gap-[8px]'>
<h3 className='min-w-0 flex-1 truncate font-medium text-[14px] text-[var(--text-primary)]'>
{title}
</h3>
{shortId && <Badge className='flex-shrink-0 rounded-[4px] text-[12px]'>{shortId}</Badge>}
</div>
const handleClick = useCallback(
(e: React.MouseEvent) => {
if (isContextMenuOpen) {
e.preventDefault()
return
}
router.push(href)
},
[isContextMenuOpen, router, href]
)
<div className='flex flex-1 flex-col gap-[8px]'>
<div className='flex items-center justify-between'>
<span className='flex items-center gap-[6px] text-[12px] text-[var(--text-tertiary)]'>
<DocumentAttachment className='h-[12px] w-[12px]' />
{docCount} {docCount === 1 ? 'doc' : 'docs'}
</span>
{updatedAt && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<span className='text-[12px] text-[var(--text-tertiary)]'>
last updated: {formatRelativeTime(updatedAt)}
</span>
</Tooltip.Trigger>
<Tooltip.Content>{formatAbsoluteDate(updatedAt)}</Tooltip.Content>
</Tooltip.Root>
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
router.push(href)
}
},
[router, href]
)
const handleOpenInNewTab = useCallback(() => {
window.open(href, '_blank')
}, [href])
const handleViewTags = useCallback(() => {
setIsTagsModalOpen(true)
}, [])
const handleEdit = useCallback(() => {
setIsEditModalOpen(true)
}, [])
const handleDelete = useCallback(() => {
setIsDeleteModalOpen(true)
}, [])
const handleConfirmDelete = useCallback(async () => {
if (!id || !onDelete) return
setIsDeleting(true)
try {
await onDelete(id)
setIsDeleteModalOpen(false)
} finally {
setIsDeleting(false)
}
}, [id, onDelete])
const handleSave = useCallback(
async (knowledgeBaseId: string, name: string, newDescription: string) => {
if (!onUpdate) return
await onUpdate(knowledgeBaseId, name, newDescription)
},
[onUpdate]
)
return (
<>
<div
role='button'
tabIndex={0}
className='h-full cursor-pointer'
onClick={handleClick}
onKeyDown={handleKeyDown}
onContextMenu={handleContextMenu}
>
<div className='group flex h-full flex-col gap-[12px] rounded-[4px] bg-[var(--surface-3)] px-[8px] py-[6px] transition-colors hover:bg-[var(--surface-4)] dark:bg-[var(--surface-4)] dark:hover:bg-[var(--surface-5)]'>
<div className='flex items-center justify-between gap-[8px]'>
<h3 className='min-w-0 flex-1 truncate font-medium text-[14px] text-[var(--text-primary)]'>
{title}
</h3>
{shortId && (
<Badge className='flex-shrink-0 rounded-[4px] text-[12px]'>{shortId}</Badge>
)}
</div>
<div className='h-0 w-full border-[var(--divider)] border-t' />
<div className='flex flex-1 flex-col gap-[8px]'>
<div className='flex items-center justify-between'>
<span className='flex items-center gap-[6px] text-[12px] text-[var(--text-tertiary)]'>
<DocumentAttachment className='h-[12px] w-[12px]' />
{docCount} {docCount === 1 ? 'doc' : 'docs'}
</span>
{updatedAt && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<span className='text-[12px] text-[var(--text-tertiary)]'>
last updated: {formatRelativeTime(updatedAt)}
</span>
</Tooltip.Trigger>
<Tooltip.Content>{formatAbsoluteDate(updatedAt)}</Tooltip.Content>
</Tooltip.Root>
)}
</div>
<p className='line-clamp-2 h-[36px] text-[12px] text-[var(--text-tertiary)] leading-[18px]'>
{description}
</p>
<div className='h-0 w-full border-[var(--divider)] border-t' />
<p className='line-clamp-2 h-[36px] text-[12px] text-[var(--text-tertiary)] leading-[18px]'>
{description}
</p>
</div>
</div>
</div>
</Link>
<KnowledgeBaseContextMenu
isOpen={isContextMenuOpen}
position={contextMenuPosition}
menuRef={menuRef}
onClose={closeContextMenu}
onOpenInNewTab={handleOpenInNewTab}
onViewTags={handleViewTags}
onEdit={handleEdit}
onDelete={handleDelete}
showOpenInNewTab={true}
showViewTags={!!id}
showEdit={!!onUpdate}
showDelete={!!onDelete}
disableEdit={!userPermissions.canEdit}
disableDelete={!userPermissions.canEdit}
/>
{id && onUpdate && (
<EditKnowledgeBaseModal
open={isEditModalOpen}
onOpenChange={setIsEditModalOpen}
knowledgeBaseId={id}
initialName={title}
initialDescription={description === 'No description provided' ? '' : description}
onSave={handleSave}
/>
)}
{id && onDelete && (
<DeleteKnowledgeBaseModal
isOpen={isDeleteModalOpen}
onClose={() => setIsDeleteModalOpen(false)}
onConfirm={handleConfirmDelete}
isDeleting={isDeleting}
knowledgeBaseName={title}
/>
)}
{id && (
<BaseTagsModal
open={isTagsModalOpen}
onOpenChange={setIsTagsModalOpen}
knowledgeBaseId={id}
/>
)}
</>
)
}

View File

@@ -0,0 +1,68 @@
'use client'
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn'
interface DeleteKnowledgeBaseModalProps {
/**
* Whether the modal is open
*/
isOpen: boolean
/**
* Callback when modal should close
*/
onClose: () => void
/**
* Callback when delete is confirmed
*/
onConfirm: () => void
/**
* Whether the delete operation is in progress
*/
isDeleting: boolean
/**
* Name of the knowledge base being deleted
*/
knowledgeBaseName?: string
}
/**
* Delete confirmation modal for knowledge base items.
* Displays a warning message and confirmation buttons.
*/
export function DeleteKnowledgeBaseModal({
isOpen,
onClose,
onConfirm,
isDeleting,
knowledgeBaseName,
}: DeleteKnowledgeBaseModalProps) {
return (
<Modal open={isOpen} onOpenChange={onClose}>
<ModalContent className='w-[400px]'>
<ModalHeader>Delete Knowledge Base</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
{knowledgeBaseName ? (
<>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{knowledgeBaseName}</span>?
This will permanently remove all associated documents, chunks, and embeddings.
</>
) : (
'Are you sure you want to delete this knowledge base? This will permanently remove all associated documents, chunks, and embeddings.'
)}{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p>
</ModalBody>
<ModalFooter>
<Button variant='active' onClick={onClose} disabled={isDeleting}>
Cancel
</Button>
<Button variant='destructive' onClick={onConfirm} disabled={isDeleting}>
{isDeleting ? 'Deleting...' : 'Delete'}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)
}

View File

@@ -0,0 +1,175 @@
'use client'
import { useEffect, useState } from 'react'
import { zodResolver } from '@hookform/resolvers/zod'
import { createLogger } from '@sim/logger'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import {
Button,
Input,
Label,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Textarea,
} from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
const logger = createLogger('EditKnowledgeBaseModal')
interface EditKnowledgeBaseModalProps {
open: boolean
onOpenChange: (open: boolean) => void
knowledgeBaseId: string
initialName: string
initialDescription: string
onSave: (id: string, name: string, description: string) => Promise<void>
}
const FormSchema = z.object({
name: z
.string()
.min(1, 'Name is required')
.max(100, 'Name must be less than 100 characters')
.refine((value) => value.trim().length > 0, 'Name cannot be empty'),
description: z.string().max(500, 'Description must be less than 500 characters').optional(),
})
type FormValues = z.infer<typeof FormSchema>
/**
* Modal for editing knowledge base name and description
*/
export function EditKnowledgeBaseModal({
open,
onOpenChange,
knowledgeBaseId,
initialName,
initialDescription,
onSave,
}: EditKnowledgeBaseModalProps) {
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
const {
register,
handleSubmit,
reset,
watch,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(FormSchema),
defaultValues: {
name: initialName,
description: initialDescription,
},
mode: 'onSubmit',
})
const nameValue = watch('name')
useEffect(() => {
if (open) {
setError(null)
reset({
name: initialName,
description: initialDescription,
})
}
}, [open, initialName, initialDescription, reset])
const onSubmit = async (data: FormValues) => {
setIsSubmitting(true)
setError(null)
try {
await onSave(knowledgeBaseId, data.name.trim(), data.description?.trim() || '')
onOpenChange(false)
} catch (err) {
logger.error('Error updating knowledge base:', err)
setError(err instanceof Error ? err.message : 'Failed to update knowledge base')
} finally {
setIsSubmitting(false)
}
}
return (
<Modal open={open} onOpenChange={onOpenChange}>
<ModalContent>
<ModalHeader>Edit Knowledge Base</ModalHeader>
<form onSubmit={handleSubmit(onSubmit)} className='flex min-h-0 flex-1 flex-col'>
<ModalBody className='!pb-[16px]'>
<div className='space-y-[12px]'>
<div className='flex flex-col gap-[8px]'>
<Label htmlFor='kb-name'>Name</Label>
<Input
id='kb-name'
placeholder='Enter knowledge base name'
{...register('name')}
className={cn(errors.name && 'border-[var(--text-error)]')}
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
data-lpignore='true'
data-form-type='other'
/>
{errors.name && (
<p className='text-[11px] text-[var(--text-error)]'>{errors.name.message}</p>
)}
</div>
<div className='flex flex-col gap-[8px]'>
<Label htmlFor='description'>Description</Label>
<Textarea
id='description'
placeholder='Describe this knowledge base (optional)'
rows={3}
{...register('description')}
className={cn(errors.description && 'border-[var(--text-error)]')}
/>
{errors.description && (
<p className='text-[11px] text-[var(--text-error)]'>
{errors.description.message}
</p>
)}
</div>
</div>
</ModalBody>
<ModalFooter>
<div className='flex w-full items-center justify-between gap-[12px]'>
{error ? (
<p className='min-w-0 flex-1 truncate text-[11px] text-[var(--text-error)] leading-tight'>
{error}
</p>
) : (
<div />
)}
<div className='flex flex-shrink-0 gap-[8px]'>
<Button
variant='default'
onClick={() => onOpenChange(false)}
type='button'
disabled={isSubmitting}
>
Cancel
</Button>
<Button
variant='tertiary'
type='submit'
disabled={isSubmitting || !nameValue?.trim()}
>
{isSubmitting ? 'Saving...' : 'Save'}
</Button>
</div>
</div>
</ModalFooter>
</form>
</ModalContent>
</Modal>
)
}

View File

@@ -1,4 +1,7 @@
export { BaseCard, BaseCardSkeleton, BaseCardSkeletonGrid } from './base-card/base-card'
export { CreateBaseModal } from './create-base-modal/create-base-modal'
export { DeleteKnowledgeBaseModal } from './delete-knowledge-base-modal/delete-knowledge-base-modal'
export { EditKnowledgeBaseModal } from './edit-knowledge-base-modal/edit-knowledge-base-modal'
export { getDocumentIcon } from './icons/document-icons'
export { KnowledgeBaseContextMenu } from './knowledge-base-context-menu/knowledge-base-context-menu'
export { KnowledgeHeader } from './knowledge-header/knowledge-header'

View File

@@ -0,0 +1,147 @@
'use client'
import { Popover, PopoverAnchor, PopoverContent, PopoverItem } from '@/components/emcn'
interface KnowledgeBaseContextMenuProps {
/**
* Whether the context menu is open
*/
isOpen: boolean
/**
* Position of the context menu
*/
position: { x: number; y: number }
/**
* Ref for the menu element
*/
menuRef: React.RefObject<HTMLDivElement | null>
/**
* Callback when menu should close
*/
onClose: () => void
/**
* Callback when open in new tab is clicked
*/
onOpenInNewTab?: () => void
/**
* Callback when view tags is clicked
*/
onViewTags?: () => void
/**
* Callback when edit is clicked
*/
onEdit?: () => void
/**
* Callback when delete is clicked
*/
onDelete?: () => void
/**
* Whether to show the open in new tab option
* @default true
*/
showOpenInNewTab?: boolean
/**
* Whether to show the view tags option
* @default true
*/
showViewTags?: boolean
/**
* Whether to show the edit option
* @default true
*/
showEdit?: boolean
/**
* Whether to show the delete option
* @default true
*/
showDelete?: boolean
/**
* Whether the edit option is disabled
* @default false
*/
disableEdit?: boolean
/**
* Whether the delete option is disabled
* @default false
*/
disableDelete?: boolean
}
/**
* Context menu component for knowledge base cards.
* Displays open in new tab, view tags, edit, and delete options in a popover at the right-click position.
*/
export function KnowledgeBaseContextMenu({
isOpen,
position,
menuRef,
onClose,
onOpenInNewTab,
onViewTags,
onEdit,
onDelete,
showOpenInNewTab = true,
showViewTags = true,
showEdit = true,
showDelete = true,
disableEdit = false,
disableDelete = false,
}: KnowledgeBaseContextMenuProps) {
return (
<Popover open={isOpen} onOpenChange={onClose} variant='secondary' size='sm'>
<PopoverAnchor
style={{
position: 'fixed',
left: `${position.x}px`,
top: `${position.y}px`,
width: '1px',
height: '1px',
}}
/>
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
{showOpenInNewTab && onOpenInNewTab && (
<PopoverItem
onClick={() => {
onOpenInNewTab()
onClose()
}}
>
Open in new tab
</PopoverItem>
)}
{showViewTags && onViewTags && (
<PopoverItem
onClick={() => {
onViewTags()
onClose()
}}
>
View tags
</PopoverItem>
)}
{showEdit && onEdit && (
<PopoverItem
disabled={disableEdit}
onClick={() => {
onEdit()
onClose()
}}
>
Edit
</PopoverItem>
)}
{showDelete && onDelete && (
<PopoverItem
disabled={disableDelete}
onClick={() => {
onDelete()
onClose()
}}
>
Delete
</PopoverItem>
)}
</PopoverContent>
</Popover>
)
}

View File

@@ -1,6 +1,7 @@
'use client'
import { useMemo, useState } from 'react'
import { useCallback, useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
import { ChevronDown, Database, Search } from 'lucide-react'
import { useParams } from 'next/navigation'
import {
@@ -29,7 +30,9 @@ import {
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { useDebounce } from '@/hooks/use-debounce'
import { useKnowledgeBasesList } from '@/hooks/use-knowledge'
import type { KnowledgeBaseData } from '@/stores/knowledge/store'
import { type KnowledgeBaseData, useKnowledgeStore } from '@/stores/knowledge/store'
const logger = createLogger('Knowledge')
/**
* Extended knowledge base data with document count
@@ -46,7 +49,7 @@ export function Knowledge() {
const params = useParams()
const workspaceId = params.workspaceId as string
const { knowledgeBases, isLoading, error, addKnowledgeBase, refreshList } =
const { knowledgeBases, isLoading, error, addKnowledgeBase, removeKnowledgeBase, refreshList } =
useKnowledgeBasesList(workspaceId)
const userPermissions = useUserPermissionsContext()
@@ -85,6 +88,65 @@ export function Knowledge() {
refreshList()
}
const { updateKnowledgeBase: updateKnowledgeBaseInStore } = useKnowledgeStore()
/**
* Updates a knowledge base name and description
*/
const handleUpdateKnowledgeBase = useCallback(
async (id: string, name: string, description: string) => {
const response = await fetch(`/api/knowledge/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name, description }),
})
if (!response.ok) {
const result = await response.json()
throw new Error(result.error || 'Failed to update knowledge base')
}
const result = await response.json()
if (result.success) {
logger.info(`Knowledge base updated: ${id}`)
updateKnowledgeBaseInStore(id, { name, description })
await refreshList()
} else {
throw new Error(result.error || 'Failed to update knowledge base')
}
},
[refreshList, updateKnowledgeBaseInStore]
)
/**
* Deletes a knowledge base
*/
const handleDeleteKnowledgeBase = useCallback(
async (id: string) => {
const response = await fetch(`/api/knowledge/${id}`, {
method: 'DELETE',
})
if (!response.ok) {
const result = await response.json()
throw new Error(result.error || 'Failed to delete knowledge base')
}
const result = await response.json()
if (result.success) {
logger.info(`Knowledge base deleted: ${id}`)
removeKnowledgeBase(id)
} else {
throw new Error(result.error || 'Failed to delete knowledge base')
}
},
[removeKnowledgeBase]
)
/**
* Filter and sort knowledge bases based on search query and sort options
* Memoized to prevent unnecessary recalculations on render
@@ -234,6 +296,8 @@ export function Knowledge() {
description={displayData.description}
createdAt={displayData.createdAt}
updatedAt={displayData.updatedAt}
onUpdate={handleUpdateKnowledgeBase}
onDelete={handleDeleteKnowledgeBase}
/>
)
})

View File

@@ -15,7 +15,6 @@ import {
PopoverTrigger,
Tooltip,
} from '@/components/emcn'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { ContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu'
import { DeleteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal'
import { InviteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal'
@@ -27,6 +26,7 @@ interface Workspace {
name: string
ownerId: string
role?: string
permissions?: 'admin' | 'write' | 'read' | null
}
interface WorkspaceHeaderProps {
@@ -128,7 +128,6 @@ export function WorkspaceHeader({
isImportingWorkspace,
showCollapseButton = true,
}: WorkspaceHeaderProps) {
const userPermissions = useUserPermissionsContext()
const [isInviteModalOpen, setIsInviteModalOpen] = useState(false)
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
@@ -138,13 +137,15 @@ export function WorkspaceHeader({
const [isListRenaming, setIsListRenaming] = useState(false)
const listRenameInputRef = useRef<HTMLInputElement | null>(null)
// Context menu state
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 })
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false)
const contextMenuRef = useRef<HTMLDivElement | null>(null)
const capturedWorkspaceRef = useRef<{ id: string; name: string } | null>(null)
const capturedWorkspaceRef = useRef<{
id: string
name: string
permissions?: 'admin' | 'write' | 'read' | null
} | null>(null)
// Client-only rendering for Popover to prevent Radix ID hydration mismatch
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setIsMounted(true)
@@ -186,7 +187,11 @@ export function WorkspaceHeader({
e.preventDefault()
e.stopPropagation()
capturedWorkspaceRef.current = { id: workspace.id, name: workspace.name }
capturedWorkspaceRef.current = {
id: workspace.id,
name: workspace.name,
permissions: workspace.permissions,
}
setContextMenuPosition({ x: e.clientX, y: e.clientY })
setIsContextMenuOpen(true)
}
@@ -467,23 +472,31 @@ export function WorkspaceHeader({
</div>
{/* Context Menu */}
<ContextMenu
isOpen={isContextMenuOpen}
position={contextMenuPosition}
menuRef={contextMenuRef}
onClose={closeContextMenu}
onRename={handleRenameAction}
onDuplicate={handleDuplicateAction}
onExport={handleExportAction}
onDelete={handleDeleteAction}
showRename={true}
showDuplicate={true}
showExport={true}
disableRename={!userPermissions.canEdit}
disableDuplicate={!userPermissions.canEdit}
disableExport={!userPermissions.canAdmin}
disableDelete={!userPermissions.canAdmin}
/>
{(() => {
const capturedPermissions = capturedWorkspaceRef.current?.permissions
const contextCanEdit = capturedPermissions === 'admin' || capturedPermissions === 'write'
const contextCanAdmin = capturedPermissions === 'admin'
return (
<ContextMenu
isOpen={isContextMenuOpen}
position={contextMenuPosition}
menuRef={contextMenuRef}
onClose={closeContextMenu}
onRename={handleRenameAction}
onDuplicate={handleDuplicateAction}
onExport={handleExportAction}
onDelete={handleDeleteAction}
showRename={true}
showDuplicate={true}
showExport={true}
disableRename={!contextCanEdit}
disableDuplicate={!contextCanEdit}
disableExport={!contextCanAdmin}
disableDelete={!contextCanAdmin}
/>
)
})()}
{/* Invite Modal */}
<InviteModal