mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(tables): header "T…" flicker — emcn barrel Table component shadowed the Table icon in loading fallbacks (#5007)
This commit is contained in:
@@ -1565,7 +1565,10 @@ export function Files() {
|
||||
}, [router, workspaceId])
|
||||
|
||||
const loadingBreadcrumbs = useMemo(
|
||||
() => [{ label: 'Files', onClick: handleNavigateToFiles }, { label: '...' }],
|
||||
(): BreadcrumbItem[] => [
|
||||
{ label: 'Files', onClick: handleNavigateToFiles },
|
||||
{ label: '…', terminal: true },
|
||||
],
|
||||
[handleNavigateToFiles]
|
||||
)
|
||||
|
||||
|
||||
@@ -4,5 +4,4 @@ export { DeleteKnowledgeBaseModal } from './delete-knowledge-base-modal'
|
||||
export { EditKnowledgeBaseModal } from './edit-knowledge-base-modal'
|
||||
export { getDocumentIcon } from './icons'
|
||||
export { KnowledgeBaseContextMenu } from './knowledge-base-context-menu'
|
||||
export { KnowledgeHeader } from './knowledge-header'
|
||||
export { KnowledgeListContextMenu } from './knowledge-list-context-menu'
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { KnowledgeHeader } from './knowledge-header'
|
||||
-204
@@ -1,204 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { AlertTriangle, LibraryBig, MoreHorizontal } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Tooltip,
|
||||
} from '@/components/emcn'
|
||||
import { ChevronDown } from '@/components/emcn/icons'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { filterButtonClass } from '@/app/workspace/[workspaceId]/knowledge/components/constants'
|
||||
import { useUpdateKnowledgeBase } from '@/hooks/queries/kb/knowledge'
|
||||
import { useWorkspacesQuery } from '@/hooks/queries/workspace'
|
||||
|
||||
const logger = createLogger('KnowledgeHeader')
|
||||
|
||||
interface BreadcrumbItem {
|
||||
label: string
|
||||
href?: string
|
||||
id?: string
|
||||
}
|
||||
|
||||
const HEADER_STYLES = {
|
||||
container: 'flex items-center justify-between px-6 pt-3.5 pb-6',
|
||||
breadcrumbs: 'flex items-center gap-2',
|
||||
icon: 'size-[18px] text-[var(--text-icon)] transition-colors',
|
||||
link: 'group flex items-center gap-2 font-medium text-sm text-[var(--text-body)] transition-colors hover-hover:text-[var(--text-secondary)]',
|
||||
label: 'font-medium text-sm text-[var(--text-body)]',
|
||||
separator: 'text-[var(--text-icon)]',
|
||||
actionsContainer: 'flex items-center gap-2',
|
||||
} as const
|
||||
|
||||
interface KnowledgeHeaderOptions {
|
||||
knowledgeBaseId?: string
|
||||
currentWorkspaceId?: string | null
|
||||
onWorkspaceChange?: (workspaceId: string | null) => void | Promise<void>
|
||||
onDeleteKnowledgeBase?: () => void
|
||||
}
|
||||
|
||||
interface KnowledgeHeaderProps {
|
||||
breadcrumbs: BreadcrumbItem[]
|
||||
options?: KnowledgeHeaderOptions
|
||||
}
|
||||
|
||||
export function KnowledgeHeader({ breadcrumbs, options }: KnowledgeHeaderProps) {
|
||||
const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false)
|
||||
const [isWorkspaceMenuOpen, setIsWorkspaceMenuOpen] = useState(false)
|
||||
|
||||
const { data: allWorkspaces = [], isLoading: isLoadingWorkspaces } = useWorkspacesQuery(
|
||||
!!options?.knowledgeBaseId
|
||||
)
|
||||
const workspaces = allWorkspaces.filter(
|
||||
(ws) => ws.permissions === 'write' || ws.permissions === 'admin'
|
||||
)
|
||||
|
||||
const updateKnowledgeBase = useUpdateKnowledgeBase()
|
||||
|
||||
const handleWorkspaceChange = async (workspaceId: string | null) => {
|
||||
if (updateKnowledgeBase.isPending || !options?.knowledgeBaseId) return
|
||||
|
||||
setIsWorkspaceMenuOpen(false)
|
||||
|
||||
updateKnowledgeBase.mutate(
|
||||
{
|
||||
knowledgeBaseId: options.knowledgeBaseId,
|
||||
updates: { workspaceId },
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
logger.info(
|
||||
`Knowledge base workspace updated: ${options.knowledgeBaseId} -> ${workspaceId}`
|
||||
)
|
||||
options.onWorkspaceChange?.(workspaceId)
|
||||
},
|
||||
onError: (err) => {
|
||||
logger.error('Error updating workspace:', err)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const currentWorkspace = workspaces.find((ws) => ws.id === options?.currentWorkspaceId)
|
||||
const hasWorkspace = !!options?.currentWorkspaceId
|
||||
|
||||
return (
|
||||
<div className={HEADER_STYLES.container}>
|
||||
<div className={HEADER_STYLES.breadcrumbs}>
|
||||
{breadcrumbs.map((breadcrumb, index) => {
|
||||
const key = breadcrumb.id || `${breadcrumb.label}-${breadcrumb.href || index}`
|
||||
|
||||
return (
|
||||
<div key={key} className='flex items-center gap-2'>
|
||||
{index === 0 && <LibraryBig className={HEADER_STYLES.icon} />}
|
||||
|
||||
{breadcrumb.href ? (
|
||||
<Link href={breadcrumb.href} prefetch={true} className={HEADER_STYLES.link}>
|
||||
<span>{breadcrumb.label}</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span className={HEADER_STYLES.label}>{breadcrumb.label}</span>
|
||||
)}
|
||||
|
||||
{index < breadcrumbs.length - 1 && <span className={HEADER_STYLES.separator}>/</span>}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Actions Area */}
|
||||
{options && (
|
||||
<div className={HEADER_STYLES.actionsContainer}>
|
||||
{/* Workspace Selector */}
|
||||
{options.knowledgeBaseId && (
|
||||
<div className='flex items-center gap-2'>
|
||||
{/* Warning icon for unassigned knowledge bases */}
|
||||
{!hasWorkspace && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<AlertTriangle className='size-4 text-amber-500' />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='top'>Not assigned to workspace</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
|
||||
{/* Workspace selector dropdown */}
|
||||
<DropdownMenu open={isWorkspaceMenuOpen} onOpenChange={setIsWorkspaceMenuOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
disabled={isLoadingWorkspaces || updateKnowledgeBase.isPending}
|
||||
className={filterButtonClass}
|
||||
>
|
||||
<span className='truncate'>
|
||||
{isLoadingWorkspaces
|
||||
? 'Loading...'
|
||||
: updateKnowledgeBase.isPending
|
||||
? 'Updating...'
|
||||
: currentWorkspace?.name || 'No workspace'}
|
||||
</span>
|
||||
<ChevronDown className='ml-2 size-4 text-[var(--text-icon)]' />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end' side='bottom' sideOffset={4}>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={!options.currentWorkspaceId}
|
||||
onSelect={() => handleWorkspaceChange(null)}
|
||||
>
|
||||
<span className='text-[var(--text-secondary)]'>No workspace</span>
|
||||
</DropdownMenuCheckboxItem>
|
||||
|
||||
{workspaces.map((workspace) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={workspace.id}
|
||||
checked={options.currentWorkspaceId === workspace.id}
|
||||
onSelect={() => handleWorkspaceChange(workspace.id)}
|
||||
>
|
||||
{workspace.name}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
|
||||
{workspaces.length === 0 && !isLoadingWorkspaces && (
|
||||
<DropdownMenuItem disabled>
|
||||
<span className='text-[var(--text-secondary)] text-xs'>
|
||||
No workspaces with write access
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions Menu */}
|
||||
{options.onDeleteKnowledgeBase && (
|
||||
<DropdownMenu open={isActionsMenuOpen} onOpenChange={setIsActionsMenuOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
className={filterButtonClass}
|
||||
aria-label='Knowledge base actions menu'
|
||||
>
|
||||
<MoreHorizontal className='size-4' />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end' side='bottom' sideOffset={4}>
|
||||
<DropdownMenuItem onSelect={() => options.onDeleteKnowledgeBase?.()}>
|
||||
<Trash className='size-4' />
|
||||
<span>Delete Knowledge Base</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { Table as TableIcon } from '@/components/emcn'
|
||||
import { Table as TableIcon } from '@/components/emcn/icons'
|
||||
import {
|
||||
type BreadcrumbItem,
|
||||
ResourceChromeFallback,
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { ColumnDefinition, Filter, TableRow as TableRowType, WorkflowGroup
|
||||
import { getColumnId } from '@/lib/table/column-keys'
|
||||
import { TABLE_LIMITS } from '@/lib/table/constants'
|
||||
import {
|
||||
type BreadcrumbItem,
|
||||
type ColumnOption,
|
||||
Resource,
|
||||
type SortConfig,
|
||||
@@ -485,34 +486,36 @@ export function Table({
|
||||
}
|
||||
|
||||
const breadcrumbs = useMemo(
|
||||
() => [
|
||||
(): BreadcrumbItem[] => [
|
||||
{ label: 'Tables', onClick: handleNavigateBack },
|
||||
{
|
||||
label: tableData?.name ?? '',
|
||||
editing: tableHeaderRename.editingId
|
||||
? {
|
||||
isEditing: true,
|
||||
value: tableHeaderRename.editValue,
|
||||
onChange: tableHeaderRename.setEditValue,
|
||||
onSubmit: tableHeaderRename.submitRename,
|
||||
onCancel: tableHeaderRename.cancelRename,
|
||||
}
|
||||
: undefined,
|
||||
dropdownItems: [
|
||||
{
|
||||
label: 'Rename',
|
||||
icon: Pencil,
|
||||
disabled: !tableData,
|
||||
onClick: handleStartTableRename,
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
icon: Trash,
|
||||
disabled: !tableData,
|
||||
onClick: onRequestDeleteTable,
|
||||
},
|
||||
],
|
||||
},
|
||||
// While the table loads, mirror this route's loading.tsx (terminal "…" crumb)
|
||||
// so no empty-label / orphaned-chevron frame renders in between.
|
||||
tableData
|
||||
? {
|
||||
label: tableData.name,
|
||||
editing: tableHeaderRename.editingId
|
||||
? {
|
||||
isEditing: true,
|
||||
value: tableHeaderRename.editValue,
|
||||
onChange: tableHeaderRename.setEditValue,
|
||||
onSubmit: tableHeaderRename.submitRename,
|
||||
onCancel: tableHeaderRename.cancelRename,
|
||||
}
|
||||
: undefined,
|
||||
dropdownItems: [
|
||||
{
|
||||
label: 'Rename',
|
||||
icon: Pencil,
|
||||
onClick: handleStartTableRename,
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
icon: Trash,
|
||||
onClick: onRequestDeleteTable,
|
||||
},
|
||||
],
|
||||
}
|
||||
: { label: '…', terminal: true },
|
||||
],
|
||||
[
|
||||
handleNavigateBack,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { Plus, Table as TableIcon, Upload } from '@/components/emcn'
|
||||
import { Plus, Upload } from '@/components/emcn'
|
||||
import { Table as TableIcon } from '@/components/emcn/icons'
|
||||
import {
|
||||
type ChromeActionSpec,
|
||||
ResourceChromeFallback,
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
export * from './components'
|
||||
/**
|
||||
* `Table` exists in BOTH `./components` (data-table element) and `./icons`
|
||||
* (glyph). This explicit re-export resolves the ambiguity to the COMPONENT —
|
||||
* always import the icon from `@/components/emcn/icons`. Rendering the
|
||||
* component as an icon paints an empty `w-full` table that squeezes its
|
||||
* siblings (shipped as the tables-header "T…" flicker).
|
||||
*/
|
||||
export {
|
||||
Table,
|
||||
TableBody,
|
||||
|
||||
Reference in New Issue
Block a user