From 08cda72bf63cc7aef634db02b542233e690e1929 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 29 Dec 2025 18:48:39 -0800 Subject: [PATCH] feat(kb): added permissions to workspace popover, added kb popover to view tags, edit description and kb name --- .../components/base-card/base-card.tsx | 208 +++++++++++++++--- .../delete-knowledge-base-modal.tsx | 68 ++++++ .../edit-knowledge-base-modal.tsx | 175 +++++++++++++++ .../knowledge/components/index.ts | 3 + .../knowledge-base-context-menu.tsx | 147 +++++++++++++ .../[workspaceId]/knowledge/knowledge.tsx | 70 +++++- .../workspace-header/workspace-header.tsx | 59 +++-- 7 files changed, 672 insertions(+), 58 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/components/delete-knowledge-base-modal/delete-knowledge-base-modal.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/components/edit-knowledge-base-modal/edit-knowledge-base-modal.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-base-context-menu/knowledge-base-context-menu.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-card/base-card.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-card/base-card.tsx index 64782d86b0..729064aba1 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-card/base-card.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-card/base-card.tsx @@ -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 + onDelete?: (id: string) => Promise } /** @@ -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 ( - -
-
-

- {title} -

- {shortId && {shortId}} -
+ const handleClick = useCallback( + (e: React.MouseEvent) => { + if (isContextMenuOpen) { + e.preventDefault() + return + } + router.push(href) + }, + [isContextMenuOpen, router, href] + ) + + 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]) -
-
- - - {docCount} {docCount === 1 ? 'doc' : 'docs'} - - {updatedAt && ( - - - - last updated: {formatRelativeTime(updatedAt)} - - - {formatAbsoluteDate(updatedAt)} - + const handleSave = useCallback( + async (knowledgeBaseId: string, name: string, newDescription: string) => { + if (!onUpdate) return + await onUpdate(knowledgeBaseId, name, newDescription) + }, + [onUpdate] + ) + + return ( + <> +
+
+
+

+ {title} +

+ {shortId && ( + {shortId} )}
-
+
+
+ + + {docCount} {docCount === 1 ? 'doc' : 'docs'} + + {updatedAt && ( + + + + last updated: {formatRelativeTime(updatedAt)} + + + {formatAbsoluteDate(updatedAt)} + + )} +
+ +
-

- {description} -

+

+ {description} +

+
- + + + + {id && onUpdate && ( + + )} + + {id && onDelete && ( + setIsDeleteModalOpen(false)} + onConfirm={handleConfirmDelete} + isDeleting={isDeleting} + knowledgeBaseName={title} + /> + )} + + {id && ( + + )} + ) } diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/delete-knowledge-base-modal/delete-knowledge-base-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/delete-knowledge-base-modal/delete-knowledge-base-modal.tsx new file mode 100644 index 0000000000..3a0d146d15 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/delete-knowledge-base-modal/delete-knowledge-base-modal.tsx @@ -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 ( + + + Delete Knowledge Base + +

+ {knowledgeBaseName ? ( + <> + Are you sure you want to delete{' '} + {knowledgeBaseName}? + 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.' + )}{' '} + This action cannot be undone. +

+
+ + + + +
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/edit-knowledge-base-modal/edit-knowledge-base-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/edit-knowledge-base-modal/edit-knowledge-base-modal.tsx new file mode 100644 index 0000000000..ef9c575308 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/edit-knowledge-base-modal/edit-knowledge-base-modal.tsx @@ -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 +} + +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 + +/** + * 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(null) + + const { + register, + handleSubmit, + reset, + watch, + formState: { errors }, + } = useForm({ + 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 ( + + + Edit Knowledge Base + +
+ +
+
+ + + {errors.name && ( +

{errors.name.message}

+ )} +
+ +
+ +