mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
feat: update sidebar and knowledge (#3804)
* feat: update sidebar and knowledge * chore: fix rernders on knowledge * chore: fix review changes * chore: fix review changes
This commit is contained in:
@@ -1,7 +1,13 @@
|
||||
import { memo } from 'react'
|
||||
import type { ResourceCell } from '@/app/workspace/[workspaceId]/components/resource/resource'
|
||||
import type { WorkspaceMember } from '@/hooks/queries/workspace'
|
||||
|
||||
function OwnerAvatar({ name, image }: { name: string; image: string | null }) {
|
||||
interface OwnerAvatarProps {
|
||||
name: string
|
||||
image: string | null
|
||||
}
|
||||
|
||||
const OwnerAvatar = memo(function OwnerAvatar({ name, image }: OwnerAvatarProps) {
|
||||
if (image) {
|
||||
return (
|
||||
<img
|
||||
@@ -18,7 +24,7 @@ function OwnerAvatar({ name, image }: { name: string; image: string | null }) {
|
||||
{name.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Resolves a user ID into a ResourceCell with an avatar icon and display name.
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { InlineRenameInput } from '@/app/workspace/[workspaceId]/components/inline-rename-input'
|
||||
|
||||
const HEADER_PLUS_ICON = <Plus className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
|
||||
export interface DropdownOption {
|
||||
label: string
|
||||
icon?: React.ElementType
|
||||
@@ -122,7 +124,7 @@ export const ResourceHeader = memo(function ResourceHeader({
|
||||
variant='subtle'
|
||||
className='px-2 py-1 text-caption'
|
||||
>
|
||||
<Plus className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
{HEADER_PLUS_ICON}
|
||||
{create.label}
|
||||
</Button>
|
||||
)}
|
||||
@@ -132,19 +134,21 @@ export const ResourceHeader = memo(function ResourceHeader({
|
||||
)
|
||||
})
|
||||
|
||||
function BreadcrumbSegment({
|
||||
icon: Icon,
|
||||
label,
|
||||
onClick,
|
||||
dropdownItems,
|
||||
editing,
|
||||
}: {
|
||||
interface BreadcrumbSegmentProps {
|
||||
icon?: React.ElementType
|
||||
label: string
|
||||
onClick?: () => void
|
||||
dropdownItems?: DropdownOption[]
|
||||
editing?: BreadcrumbEditing
|
||||
}) {
|
||||
}
|
||||
|
||||
const BreadcrumbSegment = memo(function BreadcrumbSegment({
|
||||
icon: Icon,
|
||||
label,
|
||||
onClick,
|
||||
dropdownItems,
|
||||
editing,
|
||||
}: BreadcrumbSegmentProps) {
|
||||
if (editing?.isEditing) {
|
||||
return (
|
||||
<span className='inline-flex items-center px-2 py-1'>
|
||||
@@ -203,4 +207,4 @@ function BreadcrumbSegment({
|
||||
{content}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, type ReactNode } from 'react'
|
||||
import { memo, type ReactNode, useCallback, useRef, useState } from 'react'
|
||||
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
||||
import {
|
||||
ArrowDown,
|
||||
@@ -16,6 +16,12 @@ import {
|
||||
} from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
|
||||
const SEARCH_ICON = (
|
||||
<Search className='pointer-events-none h-[14px] w-[14px] shrink-0 text-[var(--text-icon)]' />
|
||||
)
|
||||
const FILTER_ICON = <ListFilter className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
const SORT_ICON = <ArrowUpDown className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
|
||||
type SortDirection = 'asc' | 'desc'
|
||||
|
||||
export interface ColumnOption {
|
||||
@@ -79,56 +85,7 @@ export const ResourceOptionsBar = memo(function ResourceOptionsBar({
|
||||
return (
|
||||
<div className={cn('border-[var(--border)] border-b py-2.5', search ? 'px-6' : 'px-4')}>
|
||||
<div className='flex items-center justify-between'>
|
||||
{search && (
|
||||
<div className='relative flex flex-1 items-center'>
|
||||
<Search className='pointer-events-none h-[14px] w-[14px] shrink-0 text-[var(--text-icon)]' />
|
||||
<div className='flex flex-1 items-center gap-1.5 overflow-x-auto pl-2.5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'>
|
||||
{search.tags?.map((tag, i) => (
|
||||
<Button
|
||||
key={`${tag.label}-${tag.value}-${i}`}
|
||||
variant='subtle'
|
||||
className={cn(
|
||||
'shrink-0 px-2 py-1 text-caption',
|
||||
search.highlightedTagIndex === i &&
|
||||
'ring-1 ring-[var(--border-focus)] ring-offset-1'
|
||||
)}
|
||||
onClick={tag.onRemove}
|
||||
>
|
||||
{tag.label}: {tag.value}
|
||||
<span className='ml-1 text-[var(--text-icon)] text-micro'>✕</span>
|
||||
</Button>
|
||||
))}
|
||||
<input
|
||||
ref={search.inputRef}
|
||||
type='text'
|
||||
value={search.value}
|
||||
onChange={(e) => search.onChange(e.target.value)}
|
||||
onKeyDown={search.onKeyDown}
|
||||
onFocus={search.onFocus}
|
||||
onBlur={search.onBlur}
|
||||
placeholder={search.tags?.length ? '' : (search.placeholder ?? 'Search...')}
|
||||
className='min-w-[80px] flex-1 bg-transparent py-1 text-[var(--text-secondary)] text-caption outline-none placeholder:text-[var(--text-subtle)]'
|
||||
/>
|
||||
</div>
|
||||
{search.tags?.length || search.value ? (
|
||||
<button
|
||||
type='button'
|
||||
className='mr-0.5 flex h-[14px] w-[14px] shrink-0 items-center justify-center text-[var(--text-subtle)] transition-colors hover-hover:text-[var(--text-secondary)]'
|
||||
onClick={search.onClearAll}
|
||||
>
|
||||
<span className='text-caption'>✕</span>
|
||||
</button>
|
||||
) : null}
|
||||
{search.dropdown && (
|
||||
<div
|
||||
ref={search.dropdownRef}
|
||||
className='absolute top-full left-0 z-50 mt-1.5 w-full rounded-lg border border-[var(--border)] bg-[var(--bg)] shadow-sm'
|
||||
>
|
||||
{search.dropdown}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{search && <SearchSection search={search} />}
|
||||
<div className='flex items-center gap-1.5'>
|
||||
{extras}
|
||||
{filterTags?.map((tag) => (
|
||||
@@ -146,7 +103,7 @@ export const ResourceOptionsBar = memo(function ResourceOptionsBar({
|
||||
<PopoverPrimitive.Root>
|
||||
<PopoverPrimitive.Trigger asChild>
|
||||
<Button variant='subtle' className='px-2 py-1 text-caption'>
|
||||
<ListFilter className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
{FILTER_ICON}
|
||||
Filter
|
||||
</Button>
|
||||
</PopoverPrimitive.Trigger>
|
||||
@@ -170,14 +127,94 @@ export const ResourceOptionsBar = memo(function ResourceOptionsBar({
|
||||
)
|
||||
})
|
||||
|
||||
function SortDropdown({ config }: { config: SortConfig }) {
|
||||
const SearchSection = memo(function SearchSection({ search }: { search: SearchConfig }) {
|
||||
const [localValue, setLocalValue] = useState(search.value)
|
||||
|
||||
const lastReportedRef = useRef(search.value)
|
||||
|
||||
if (search.value !== lastReportedRef.current) {
|
||||
setLocalValue(search.value)
|
||||
lastReportedRef.current = search.value
|
||||
}
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const next = e.target.value
|
||||
setLocalValue(next)
|
||||
search.onChange(next)
|
||||
},
|
||||
[search.onChange]
|
||||
)
|
||||
|
||||
const handleClearAll = useCallback(() => {
|
||||
setLocalValue('')
|
||||
lastReportedRef.current = ''
|
||||
if (search.onClearAll) {
|
||||
search.onClearAll()
|
||||
} else {
|
||||
search.onChange('')
|
||||
}
|
||||
}, [search.onClearAll, search.onChange])
|
||||
|
||||
return (
|
||||
<div className='relative flex flex-1 items-center'>
|
||||
{SEARCH_ICON}
|
||||
<div className='flex flex-1 items-center gap-1.5 overflow-x-auto pl-2.5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'>
|
||||
{search.tags?.map((tag, i) => (
|
||||
<Button
|
||||
key={`${tag.label}-${tag.value}-${i}`}
|
||||
variant='subtle'
|
||||
className={cn(
|
||||
'shrink-0 px-2 py-1 text-caption',
|
||||
search.highlightedTagIndex === i && 'ring-1 ring-[var(--border-focus)] ring-offset-1'
|
||||
)}
|
||||
onClick={tag.onRemove}
|
||||
>
|
||||
{tag.label}: {tag.value}
|
||||
<span className='ml-1 text-[var(--text-icon)] text-micro'>✕</span>
|
||||
</Button>
|
||||
))}
|
||||
<input
|
||||
ref={search.inputRef}
|
||||
type='text'
|
||||
value={localValue}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={search.onKeyDown}
|
||||
onFocus={search.onFocus}
|
||||
onBlur={search.onBlur}
|
||||
placeholder={search.tags?.length ? '' : (search.placeholder ?? 'Search...')}
|
||||
className='min-w-[80px] flex-1 bg-transparent py-1 text-[var(--text-secondary)] text-caption outline-none placeholder:text-[var(--text-subtle)]'
|
||||
/>
|
||||
</div>
|
||||
{search.tags?.length || localValue ? (
|
||||
<button
|
||||
type='button'
|
||||
className='mr-0.5 flex h-[14px] w-[14px] shrink-0 items-center justify-center text-[var(--text-subtle)] transition-colors hover-hover:text-[var(--text-secondary)]'
|
||||
onClick={handleClearAll}
|
||||
>
|
||||
<span className='text-caption'>✕</span>
|
||||
</button>
|
||||
) : null}
|
||||
{search.dropdown && (
|
||||
<div
|
||||
ref={search.dropdownRef}
|
||||
className='absolute top-full left-0 z-50 mt-1.5 w-full rounded-lg border border-[var(--border)] bg-[var(--bg)] shadow-sm'
|
||||
>
|
||||
{search.dropdown}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const SortDropdown = memo(function SortDropdown({ config }: { config: SortConfig }) {
|
||||
const { options, active, onSort, onClear } = config
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant='subtle' className='px-2 py-1 text-caption'>
|
||||
<ArrowUpDown className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
{SORT_ICON}
|
||||
Sort
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -218,4 +255,4 @@ function SortDropdown({ config }: { config: SortConfig }) {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -8,6 +8,8 @@ import { ResourceHeader } from './components/resource-header'
|
||||
import type { FilterTag, SearchConfig, SortConfig } from './components/resource-options-bar'
|
||||
import { ResourceOptionsBar } from './components/resource-options-bar'
|
||||
|
||||
const CREATE_ROW_PLUS_ICON = <Plus className='h-[14px] w-[14px] text-[var(--text-subtle)]' />
|
||||
|
||||
export interface ResourceColumn {
|
||||
id: string
|
||||
header: string
|
||||
@@ -69,11 +71,13 @@ interface ResourceProps {
|
||||
const EMPTY_CELL_PLACEHOLDER = '- - -'
|
||||
const SKELETON_ROW_COUNT = 5
|
||||
|
||||
const stopPropagation = (e: React.MouseEvent) => e.stopPropagation()
|
||||
|
||||
/**
|
||||
* Shared page shell for resource list pages (tables, files, knowledge, schedules, logs).
|
||||
* Renders the header, toolbar with search, and a data table from column/row definitions.
|
||||
*/
|
||||
export function Resource({
|
||||
export const Resource = memo(function Resource({
|
||||
icon,
|
||||
title,
|
||||
breadcrumbs,
|
||||
@@ -135,7 +139,7 @@ export function Resource({
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export interface ResourceTableProps {
|
||||
columns: ResourceColumn[]
|
||||
@@ -229,6 +233,13 @@ export const ResourceTable = memo(function ResourceTable({
|
||||
const hasCheckbox = selectable != null
|
||||
const totalColSpan = columns.length + (hasCheckbox ? 1 : 0)
|
||||
|
||||
const handleSelectAll = useCallback(
|
||||
(checked: boolean | 'indeterminate') => {
|
||||
selectable?.onSelectAll(checked as boolean)
|
||||
},
|
||||
[selectable]
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<DataTableSkeleton
|
||||
@@ -259,7 +270,7 @@ export const ResourceTable = memo(function ResourceTable({
|
||||
<Checkbox
|
||||
size='sm'
|
||||
checked={selectable.isAllSelected}
|
||||
onCheckedChange={(checked) => selectable.onSelectAll(checked as boolean)}
|
||||
onCheckedChange={handleSelectAll}
|
||||
disabled={selectable.disabled}
|
||||
aria-label='Select all'
|
||||
/>
|
||||
@@ -306,68 +317,20 @@ export const ResourceTable = memo(function ResourceTable({
|
||||
<table className='w-full table-fixed text-small'>
|
||||
<ResourceColGroup columns={columns} hasCheckbox={hasCheckbox} />
|
||||
<tbody>
|
||||
{displayRows.map((row) => {
|
||||
const isSelected = selectable?.selectedIds.has(row.id) ?? false
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
data-resource-row
|
||||
data-row-id={row.id}
|
||||
className={cn(
|
||||
'transition-colors hover-hover:bg-[var(--surface-3)]',
|
||||
onRowClick && 'cursor-pointer',
|
||||
(selectedRowId === row.id || isSelected) && 'bg-[var(--surface-3)]'
|
||||
)}
|
||||
onClick={() => onRowClick?.(row.id)}
|
||||
onMouseEnter={onRowHover ? () => onRowHover(row.id) : undefined}
|
||||
onContextMenu={(e) => onRowContextMenu?.(e, row.id)}
|
||||
>
|
||||
{hasCheckbox && (
|
||||
<td className='w-[52px] py-2.5 pr-0 pl-5 align-middle'>
|
||||
<Checkbox
|
||||
size='sm'
|
||||
checked={isSelected}
|
||||
onCheckedChange={(checked) =>
|
||||
selectable.onSelectRow(row.id, checked as boolean)
|
||||
}
|
||||
disabled={selectable.disabled}
|
||||
aria-label='Select row'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{columns.map((col, colIdx) => {
|
||||
const cell = row.cells[col.id]
|
||||
return (
|
||||
<td key={col.id} className='px-6 py-2.5 align-middle'>
|
||||
<CellContent
|
||||
cell={{ ...cell, label: cell?.label || EMPTY_CELL_PLACEHOLDER }}
|
||||
primary={colIdx === 0}
|
||||
/>
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
{create && (
|
||||
<tr
|
||||
className={cn(
|
||||
'transition-colors',
|
||||
create.disabled
|
||||
? 'cursor-not-allowed'
|
||||
: 'cursor-pointer hover-hover:bg-[var(--surface-3)]'
|
||||
)}
|
||||
onClick={create.disabled ? undefined : create.onClick}
|
||||
>
|
||||
<td colSpan={totalColSpan} className='px-6 py-2.5 align-middle'>
|
||||
<span className='flex items-center gap-3 font-medium text-[var(--text-secondary)] text-sm'>
|
||||
<Plus className='h-[14px] w-[14px] text-[var(--text-subtle)]' />
|
||||
{create.label}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{displayRows.map((row) => (
|
||||
<DataRow
|
||||
key={row.id}
|
||||
row={row}
|
||||
columns={columns}
|
||||
selectedRowId={selectedRowId}
|
||||
selectable={selectable}
|
||||
onRowClick={onRowClick}
|
||||
onRowHover={onRowHover}
|
||||
onRowContextMenu={onRowContextMenu}
|
||||
hasCheckbox={hasCheckbox}
|
||||
/>
|
||||
))}
|
||||
{create && <CreateRow create={create} totalColSpan={totalColSpan} />}
|
||||
</tbody>
|
||||
</table>
|
||||
{hasMore && (
|
||||
@@ -390,7 +353,7 @@ export const ResourceTable = memo(function ResourceTable({
|
||||
)
|
||||
})
|
||||
|
||||
function Pagination({
|
||||
const Pagination = memo(function Pagination({
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
@@ -447,10 +410,17 @@ function Pagination({
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
interface CellContentProps {
|
||||
icon?: ReactNode
|
||||
label: string
|
||||
content?: ReactNode
|
||||
primary?: boolean
|
||||
}
|
||||
|
||||
function CellContent({ cell, primary }: { cell: ResourceCell; primary?: boolean }) {
|
||||
if (cell.content) return <>{cell.content}</>
|
||||
const CellContent = memo(function CellContent({ icon, label, content, primary }: CellContentProps) {
|
||||
if (content) return <>{content}</>
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
@@ -458,19 +428,132 @@ function CellContent({ cell, primary }: { cell: ResourceCell; primary?: boolean
|
||||
primary ? 'text-[var(--text-body)]' : 'text-[var(--text-secondary)]'
|
||||
)}
|
||||
>
|
||||
{cell.icon && <span className='flex-shrink-0 text-[var(--text-icon)]'>{cell.icon}</span>}
|
||||
<span className='truncate'>{cell.label}</span>
|
||||
{icon && <span className='flex-shrink-0 text-[var(--text-icon)]'>{icon}</span>}
|
||||
<span className='truncate'>{label}</span>
|
||||
</span>
|
||||
)
|
||||
})
|
||||
|
||||
interface DataRowProps {
|
||||
row: ResourceRow
|
||||
columns: ResourceColumn[]
|
||||
selectedRowId?: string | null
|
||||
selectable?: SelectableConfig
|
||||
onRowClick?: (rowId: string) => void
|
||||
onRowHover?: (rowId: string) => void
|
||||
onRowContextMenu?: (e: React.MouseEvent, rowId: string) => void
|
||||
hasCheckbox: boolean
|
||||
}
|
||||
|
||||
function ResourceColGroup({
|
||||
const DataRow = memo(function DataRow({
|
||||
row,
|
||||
columns,
|
||||
selectedRowId,
|
||||
selectable,
|
||||
onRowClick,
|
||||
onRowHover,
|
||||
onRowContextMenu,
|
||||
hasCheckbox,
|
||||
}: {
|
||||
}: DataRowProps) {
|
||||
const isSelected = selectable?.selectedIds.has(row.id) ?? false
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
onRowClick?.(row.id)
|
||||
}, [onRowClick, row.id])
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
onRowHover?.(row.id)
|
||||
}, [onRowHover, row.id])
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
onRowContextMenu?.(e, row.id)
|
||||
},
|
||||
[onRowContextMenu, row.id]
|
||||
)
|
||||
|
||||
const handleSelectRow = useCallback(
|
||||
(checked: boolean | 'indeterminate') => {
|
||||
selectable?.onSelectRow(row.id, checked as boolean)
|
||||
},
|
||||
[selectable, row.id]
|
||||
)
|
||||
|
||||
return (
|
||||
<tr
|
||||
data-resource-row
|
||||
data-row-id={row.id}
|
||||
className={cn(
|
||||
'transition-colors hover-hover:bg-[var(--surface-3)]',
|
||||
onRowClick && 'cursor-pointer',
|
||||
(selectedRowId === row.id || isSelected) && 'bg-[var(--surface-3)]'
|
||||
)}
|
||||
onClick={onRowClick ? handleClick : undefined}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onContextMenu={onRowContextMenu ? handleContextMenu : undefined}
|
||||
>
|
||||
{hasCheckbox && selectable && (
|
||||
<td className='w-[52px] py-2.5 pr-0 pl-5 align-middle'>
|
||||
<Checkbox
|
||||
size='sm'
|
||||
checked={isSelected}
|
||||
onCheckedChange={handleSelectRow}
|
||||
disabled={selectable.disabled}
|
||||
aria-label='Select row'
|
||||
onClick={stopPropagation}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{columns.map((col, colIdx) => {
|
||||
const cell = row.cells[col.id]
|
||||
return (
|
||||
<td key={col.id} className='px-6 py-2.5 align-middle'>
|
||||
<CellContent
|
||||
icon={cell?.icon}
|
||||
label={cell?.label || EMPTY_CELL_PLACEHOLDER}
|
||||
content={cell?.content}
|
||||
primary={colIdx === 0}
|
||||
/>
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
|
||||
interface CreateRowProps {
|
||||
create: CreateAction
|
||||
totalColSpan: number
|
||||
}
|
||||
|
||||
const CreateRow = memo(function CreateRow({ create, totalColSpan }: CreateRowProps) {
|
||||
return (
|
||||
<tr
|
||||
className={cn(
|
||||
'transition-colors',
|
||||
create.disabled ? 'cursor-not-allowed' : 'cursor-pointer hover-hover:bg-[var(--surface-3)]'
|
||||
)}
|
||||
onClick={create.disabled ? undefined : create.onClick}
|
||||
>
|
||||
<td colSpan={totalColSpan} className='px-6 py-2.5 align-middle'>
|
||||
<span className='flex items-center gap-3 font-medium text-[var(--text-secondary)] text-sm'>
|
||||
{CREATE_ROW_PLUS_ICON}
|
||||
{create.label}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
|
||||
interface ResourceColGroupProps {
|
||||
columns: ResourceColumn[]
|
||||
hasCheckbox?: boolean
|
||||
}) {
|
||||
}
|
||||
|
||||
const ResourceColGroup = memo(function ResourceColGroup({
|
||||
columns,
|
||||
hasCheckbox,
|
||||
}: ResourceColGroupProps) {
|
||||
return (
|
||||
<colgroup>
|
||||
{hasCheckbox && <col className='w-[52px]' />}
|
||||
@@ -486,17 +569,19 @@ function ResourceColGroup({
|
||||
))}
|
||||
</colgroup>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
function DataTableSkeleton({
|
||||
columns,
|
||||
rowCount,
|
||||
hasCheckbox,
|
||||
}: {
|
||||
interface DataTableSkeletonProps {
|
||||
columns: ResourceColumn[]
|
||||
rowCount: number
|
||||
hasCheckbox?: boolean
|
||||
}) {
|
||||
}
|
||||
|
||||
const DataTableSkeleton = memo(function DataTableSkeleton({
|
||||
columns,
|
||||
rowCount,
|
||||
hasCheckbox,
|
||||
}: DataTableSkeletonProps) {
|
||||
return (
|
||||
<>
|
||||
<div className='overflow-hidden'>
|
||||
@@ -549,4 +634,4 @@ function DataTableSkeleton({
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { memo, useEffect, useRef, useState } from 'react'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Loader2, RotateCcw, X } from 'lucide-react'
|
||||
@@ -78,7 +78,10 @@ interface SubmitStatus {
|
||||
message: string
|
||||
}
|
||||
|
||||
export function CreateBaseModal({ open, onOpenChange }: CreateBaseModalProps) {
|
||||
export const CreateBaseModal = memo(function CreateBaseModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: CreateBaseModalProps) {
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
|
||||
@@ -543,4 +546,4 @@ export function CreateBaseModal({ open, onOpenChange }: CreateBaseModalProps) {
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { memo } from 'react'
|
||||
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn'
|
||||
|
||||
interface DeleteKnowledgeBaseModalProps {
|
||||
@@ -29,7 +30,7 @@ interface DeleteKnowledgeBaseModalProps {
|
||||
* Delete confirmation modal for knowledge base items.
|
||||
* Displays a warning message and confirmation buttons.
|
||||
*/
|
||||
export function DeleteKnowledgeBaseModal({
|
||||
export const DeleteKnowledgeBaseModal = memo(function DeleteKnowledgeBaseModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
@@ -67,4 +68,4 @@ export function DeleteKnowledgeBaseModal({
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { memo, useEffect, useState } from 'react'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useForm } from 'react-hook-form'
|
||||
@@ -43,7 +43,7 @@ type FormValues = z.infer<typeof FormSchema>
|
||||
/**
|
||||
* Modal for editing knowledge base name and description
|
||||
*/
|
||||
export function EditKnowledgeBaseModal({
|
||||
export const EditKnowledgeBaseModal = memo(function EditKnowledgeBaseModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
knowledgeBaseId,
|
||||
@@ -172,4 +172,4 @@ export function EditKnowledgeBaseModal({
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { memo } from 'react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -30,7 +31,7 @@ interface KnowledgeBaseContextMenuProps {
|
||||
* Context menu component for knowledge base cards.
|
||||
* Displays open in new tab, view tags, edit, and delete options.
|
||||
*/
|
||||
export function KnowledgeBaseContextMenu({
|
||||
export const KnowledgeBaseContextMenu = memo(function KnowledgeBaseContextMenu({
|
||||
isOpen,
|
||||
position,
|
||||
onClose,
|
||||
@@ -114,4 +115,4 @@ export function KnowledgeBaseContextMenu({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { memo } from 'react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -20,7 +21,7 @@ interface KnowledgeListContextMenuProps {
|
||||
* Context menu component for the knowledge base list page.
|
||||
* Displays "Add knowledge base" option when right-clicking on empty space.
|
||||
*/
|
||||
export function KnowledgeListContextMenu({
|
||||
export const KnowledgeListContextMenu = memo(function KnowledgeListContextMenu({
|
||||
isOpen,
|
||||
position,
|
||||
onClose,
|
||||
@@ -58,4 +59,4 @@ export function KnowledgeListContextMenu({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { Database } from '@/components/emcn/icons'
|
||||
import type { KnowledgeBaseData } from '@/lib/knowledge/types'
|
||||
import type { ResourceColumn, ResourceRow } from '@/app/workspace/[workspaceId]/components'
|
||||
import type {
|
||||
CreateAction,
|
||||
ResourceColumn,
|
||||
ResourceRow,
|
||||
SearchConfig,
|
||||
} from '@/app/workspace/[workspaceId]/components'
|
||||
import { ownerCell, Resource, timeCell } from '@/app/workspace/[workspaceId]/components'
|
||||
import { BaseTagsModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/components'
|
||||
import {
|
||||
@@ -21,7 +26,6 @@ import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sideb
|
||||
import { useKnowledgeBasesList } from '@/hooks/kb/use-knowledge'
|
||||
import { useDeleteKnowledgeBase, useUpdateKnowledgeBase } from '@/hooks/queries/kb/knowledge'
|
||||
import { useWorkspaceMembersQuery } from '@/hooks/queries/workspace'
|
||||
import { useDebounce } from '@/hooks/use-debounce'
|
||||
|
||||
const logger = createLogger('Knowledge')
|
||||
|
||||
@@ -38,6 +42,8 @@ const COLUMNS: ResourceColumn[] = [
|
||||
{ id: 'updated', header: 'Last Updated' },
|
||||
]
|
||||
|
||||
const DATABASE_ICON = <Database className='h-[14px] w-[14px]' />
|
||||
|
||||
export function Knowledge() {
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
@@ -54,8 +60,16 @@ export function Knowledge() {
|
||||
const { mutateAsync: updateKnowledgeBaseMutation } = useUpdateKnowledgeBase(workspaceId)
|
||||
const { mutateAsync: deleteKnowledgeBaseMutation } = useDeleteKnowledgeBase(workspaceId)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const debouncedSearchQuery = useDebounce(searchQuery, 300)
|
||||
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('')
|
||||
const searchTimerRef = useRef<ReturnType<typeof setTimeout>>(null)
|
||||
|
||||
const handleSearchChange = useCallback((value: string) => {
|
||||
if (searchTimerRef.current) clearTimeout(searchTimerRef.current)
|
||||
searchTimerRef.current = setTimeout(() => {
|
||||
setDebouncedSearchQuery(value)
|
||||
}, 300)
|
||||
}, [])
|
||||
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
|
||||
|
||||
const [activeKnowledgeBase, setActiveKnowledgeBase] = useState<KnowledgeBaseWithDocCount | null>(
|
||||
@@ -69,7 +83,6 @@ export function Knowledge() {
|
||||
const {
|
||||
isOpen: isListContextMenuOpen,
|
||||
position: listContextMenuPosition,
|
||||
menuRef: listMenuRef,
|
||||
handleContextMenu: handleListContextMenu,
|
||||
closeMenu: closeListContextMenu,
|
||||
} = useContextMenu()
|
||||
@@ -77,11 +90,19 @@ export function Knowledge() {
|
||||
const {
|
||||
isOpen: isRowContextMenuOpen,
|
||||
position: rowContextMenuPosition,
|
||||
menuRef: rowMenuRef,
|
||||
handleContextMenu: handleRowCtxMenu,
|
||||
closeMenu: closeRowContextMenu,
|
||||
} = useContextMenu()
|
||||
|
||||
const isRowContextMenuOpenRef = useRef(isRowContextMenuOpen)
|
||||
isRowContextMenuOpenRef.current = isRowContextMenuOpen
|
||||
|
||||
const knowledgeBasesRef = useRef(knowledgeBases)
|
||||
knowledgeBasesRef.current = knowledgeBases
|
||||
|
||||
const activeKnowledgeBaseRef = useRef(activeKnowledgeBase)
|
||||
activeKnowledgeBaseRef.current = activeKnowledgeBase
|
||||
|
||||
const handleContentContextMenu = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
const target = e.target as HTMLElement
|
||||
@@ -96,7 +117,7 @@ export function Knowledge() {
|
||||
[handleListContextMenu]
|
||||
)
|
||||
|
||||
const handleAddKnowledgeBase = useCallback(() => {
|
||||
const handleOpenCreateModal = useCallback(() => {
|
||||
setIsCreateModalOpen(true)
|
||||
}, [])
|
||||
|
||||
@@ -132,7 +153,7 @@ export function Knowledge() {
|
||||
id: kb.id,
|
||||
cells: {
|
||||
name: {
|
||||
icon: <Database className='h-[14px] w-[14px]' />,
|
||||
icon: DATABASE_ICON,
|
||||
label: kb.name,
|
||||
},
|
||||
documents: {
|
||||
@@ -158,51 +179,98 @@ export function Knowledge() {
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(rowId: string) => {
|
||||
if (isRowContextMenuOpen) return
|
||||
const kb = knowledgeBases.find((k) => k.id === rowId)
|
||||
if (isRowContextMenuOpenRef.current) return
|
||||
const kb = knowledgeBasesRef.current.find((k) => k.id === rowId)
|
||||
if (!kb) return
|
||||
const urlParams = new URLSearchParams({ kbName: kb.name })
|
||||
router.push(`/workspace/${workspaceId}/knowledge/${rowId}?${urlParams.toString()}`)
|
||||
},
|
||||
[isRowContextMenuOpen, knowledgeBases, router, workspaceId]
|
||||
[router, workspaceId]
|
||||
)
|
||||
|
||||
const handleRowContextMenu = useCallback(
|
||||
(e: React.MouseEvent, rowId: string) => {
|
||||
const kb = knowledgeBases.find((k) => k.id === rowId) as KnowledgeBaseWithDocCount | undefined
|
||||
const kb = knowledgeBasesRef.current.find((k) => k.id === rowId) as
|
||||
| KnowledgeBaseWithDocCount
|
||||
| undefined
|
||||
setActiveKnowledgeBase(kb ?? null)
|
||||
handleRowCtxMenu(e)
|
||||
},
|
||||
[knowledgeBases, handleRowCtxMenu]
|
||||
[handleRowCtxMenu]
|
||||
)
|
||||
|
||||
const handleConfirmDelete = useCallback(async () => {
|
||||
if (!activeKnowledgeBase) return
|
||||
const kb = activeKnowledgeBaseRef.current
|
||||
if (!kb) return
|
||||
setIsDeleting(true)
|
||||
try {
|
||||
await handleDeleteKnowledgeBase(activeKnowledgeBase.id)
|
||||
await handleDeleteKnowledgeBase(kb.id)
|
||||
setIsDeleteModalOpen(false)
|
||||
setActiveKnowledgeBase(null)
|
||||
} finally {
|
||||
setIsDeleting(false)
|
||||
}
|
||||
}, [activeKnowledgeBase, handleDeleteKnowledgeBase])
|
||||
}, [handleDeleteKnowledgeBase])
|
||||
|
||||
const handleCloseDeleteModal = useCallback(() => {
|
||||
setIsDeleteModalOpen(false)
|
||||
setActiveKnowledgeBase(null)
|
||||
}, [])
|
||||
|
||||
const handleOpenInNewTab = useCallback(() => {
|
||||
const kb = activeKnowledgeBaseRef.current
|
||||
if (!kb) return
|
||||
const urlParams = new URLSearchParams({ kbName: kb.name })
|
||||
window.open(`/workspace/${workspaceId}/knowledge/${kb.id}?${urlParams.toString()}`, '_blank')
|
||||
}, [workspaceId])
|
||||
|
||||
const handleViewTags = useCallback(() => {
|
||||
setIsTagsModalOpen(true)
|
||||
}, [])
|
||||
|
||||
const handleCopyId = useCallback(() => {
|
||||
const kb = activeKnowledgeBaseRef.current
|
||||
if (kb) {
|
||||
navigator.clipboard.writeText(kb.id)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleEdit = useCallback(() => {
|
||||
setIsEditModalOpen(true)
|
||||
}, [])
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
setIsDeleteModalOpen(true)
|
||||
}, [])
|
||||
|
||||
const canEdit = userPermissions.canEdit === true
|
||||
|
||||
const createAction: CreateAction = useMemo(
|
||||
() => ({
|
||||
label: 'New base',
|
||||
onClick: handleOpenCreateModal,
|
||||
disabled: !canEdit,
|
||||
}),
|
||||
[handleOpenCreateModal, canEdit]
|
||||
)
|
||||
|
||||
const searchConfig: SearchConfig = useMemo(
|
||||
() => ({
|
||||
value: debouncedSearchQuery,
|
||||
onChange: handleSearchChange,
|
||||
onClearAll: () => handleSearchChange(''),
|
||||
placeholder: 'Search knowledge bases...',
|
||||
}),
|
||||
[handleSearchChange, debouncedSearchQuery]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Resource
|
||||
icon={Database}
|
||||
title='Knowledge Base'
|
||||
create={{
|
||||
label: 'New base',
|
||||
onClick: () => setIsCreateModalOpen(true),
|
||||
disabled: userPermissions.canEdit !== true,
|
||||
}}
|
||||
search={{
|
||||
value: searchQuery,
|
||||
onChange: setSearchQuery,
|
||||
placeholder: 'Search knowledge bases...',
|
||||
}}
|
||||
create={createAction}
|
||||
search={searchConfig}
|
||||
defaultSort='created'
|
||||
columns={COLUMNS}
|
||||
rows={rows}
|
||||
@@ -216,8 +284,8 @@ export function Knowledge() {
|
||||
isOpen={isListContextMenuOpen}
|
||||
position={listContextMenuPosition}
|
||||
onClose={closeListContextMenu}
|
||||
onAddKnowledgeBase={handleAddKnowledgeBase}
|
||||
disableAdd={userPermissions.canEdit !== true}
|
||||
onAddKnowledgeBase={handleOpenCreateModal}
|
||||
disableAdd={!canEdit}
|
||||
/>
|
||||
|
||||
{activeKnowledgeBase && (
|
||||
@@ -225,23 +293,17 @@ export function Knowledge() {
|
||||
isOpen={isRowContextMenuOpen}
|
||||
position={rowContextMenuPosition}
|
||||
onClose={closeRowContextMenu}
|
||||
onOpenInNewTab={() => {
|
||||
const urlParams = new URLSearchParams({ kbName: activeKnowledgeBase.name })
|
||||
window.open(
|
||||
`/workspace/${workspaceId}/knowledge/${activeKnowledgeBase.id}?${urlParams.toString()}`,
|
||||
'_blank'
|
||||
)
|
||||
}}
|
||||
onViewTags={() => setIsTagsModalOpen(true)}
|
||||
onCopyId={() => navigator.clipboard.writeText(activeKnowledgeBase.id)}
|
||||
onEdit={() => setIsEditModalOpen(true)}
|
||||
onDelete={() => setIsDeleteModalOpen(true)}
|
||||
onOpenInNewTab={handleOpenInNewTab}
|
||||
onViewTags={handleViewTags}
|
||||
onCopyId={handleCopyId}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
showOpenInNewTab
|
||||
showViewTags
|
||||
showEdit
|
||||
showDelete
|
||||
disableEdit={!userPermissions.canEdit}
|
||||
disableDelete={!userPermissions.canEdit}
|
||||
disableEdit={!canEdit}
|
||||
disableDelete={!canEdit}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -259,10 +321,7 @@ export function Knowledge() {
|
||||
{activeKnowledgeBase && (
|
||||
<DeleteKnowledgeBaseModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
onClose={() => {
|
||||
setIsDeleteModalOpen(false)
|
||||
setActiveKnowledgeBase(null)
|
||||
}}
|
||||
onClose={handleCloseDeleteModal}
|
||||
onConfirm={handleConfirmDelete}
|
||||
isDeleting={isDeleting}
|
||||
knowledgeBaseName={activeKnowledgeBase.name}
|
||||
|
||||
@@ -310,6 +310,11 @@ export const Sidebar = memo(function Sidebar() {
|
||||
const toggleCollapsed = useSidebarStore((state) => state.toggleCollapsed)
|
||||
const isOnWorkflowPage = !!workflowId
|
||||
|
||||
const isCollapsedRef = useRef(isCollapsed)
|
||||
useLayoutEffect(() => {
|
||||
isCollapsedRef.current = isCollapsed
|
||||
}, [isCollapsed])
|
||||
|
||||
// Delay collapsed tooltips until the width transition finishes.
|
||||
const [showCollapsedTooltips, setShowCollapsedTooltips] = useState(isCollapsed)
|
||||
|
||||
@@ -709,14 +714,14 @@ export const Sidebar = memo(function Sidebar() {
|
||||
icon: Settings,
|
||||
href: getSettingsHref(),
|
||||
onClick: () => {
|
||||
if (!isCollapsed) {
|
||||
if (!isCollapsedRef.current) {
|
||||
setSidebarWidth(SIDEBAR_WIDTH.MIN)
|
||||
}
|
||||
navigateToSettings()
|
||||
},
|
||||
},
|
||||
],
|
||||
[workspaceId, navigateToSettings, getSettingsHref, isCollapsed, setSidebarWidth]
|
||||
[navigateToSettings, getSettingsHref, setSidebarWidth]
|
||||
)
|
||||
|
||||
const handleStartTour = useCallback(() => {
|
||||
@@ -810,12 +815,12 @@ export const Sidebar = memo(function Sidebar() {
|
||||
|
||||
const navigateToPage = useCallback(
|
||||
(path: string) => {
|
||||
if (!isCollapsed) {
|
||||
if (!isCollapsedRef.current) {
|
||||
setSidebarWidth(SIDEBAR_WIDTH.MIN)
|
||||
}
|
||||
router.push(path)
|
||||
},
|
||||
[isCollapsed, setSidebarWidth, router]
|
||||
[setSidebarWidth, router]
|
||||
)
|
||||
|
||||
const handleConfirmDeleteTasks = useCallback(() => {
|
||||
@@ -1064,6 +1069,88 @@ export const Sidebar = memo(function Sidebar() {
|
||||
[importWorkspace]
|
||||
)
|
||||
|
||||
// ── Memoised elements & objects for collapsed menus ──
|
||||
// Prevents new JSX/object references on every render, which would defeat
|
||||
// React.memo on CollapsedSidebarMenu and its children.
|
||||
|
||||
const tasksCollapsedIcon = useMemo(
|
||||
() => <Blimp className='h-[16px] w-[16px] flex-shrink-0 text-[var(--text-icon)]' />,
|
||||
[]
|
||||
)
|
||||
|
||||
const workflowIconStyle = useMemo<React.CSSProperties>(
|
||||
() => ({
|
||||
backgroundColor: 'var(--text-icon)',
|
||||
borderColor: 'color-mix(in srgb, var(--text-icon) 60%, transparent)',
|
||||
backgroundClip: 'padding-box',
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const workflowsCollapsedIcon = useMemo(
|
||||
() => (
|
||||
<div
|
||||
className='h-[16px] w-[16px] flex-shrink-0 rounded-[3px] border-[2px]'
|
||||
style={workflowIconStyle}
|
||||
/>
|
||||
),
|
||||
[workflowIconStyle]
|
||||
)
|
||||
|
||||
const tasksPrimaryAction = useMemo(
|
||||
() => ({
|
||||
label: 'New task',
|
||||
onSelect: () => navigateToPage(`/workspace/${workspaceId}/home`),
|
||||
}),
|
||||
[navigateToPage, workspaceId]
|
||||
)
|
||||
|
||||
const workflowsPrimaryAction = useMemo(
|
||||
() => ({
|
||||
label: 'New workflow',
|
||||
onSelect: handleCreateWorkflow,
|
||||
}),
|
||||
[handleCreateWorkflow]
|
||||
)
|
||||
|
||||
// Stable no-op for collapsed workflow context menu delete (never changes)
|
||||
const noop = useCallback(() => {}, [])
|
||||
|
||||
// Stable callback for the "New task" button in expanded mode
|
||||
const handleNewTask = useCallback(
|
||||
() => navigateToPage(`/workspace/${workspaceId}/home`),
|
||||
[navigateToPage, workspaceId]
|
||||
)
|
||||
|
||||
// Stable callback for "See more" tasks
|
||||
const handleSeeMoreTasks = useCallback(() => setVisibleTaskCount((prev) => prev + 5), [])
|
||||
|
||||
// Stable callback for DeleteModal close
|
||||
const handleCloseTaskDeleteModal = useCallback(() => setIsTaskDeleteModalOpen(false), [])
|
||||
|
||||
// Stable handler for help modal open from dropdown
|
||||
const handleOpenHelpFromMenu = useCallback(() => setIsHelpModalOpen(true), [])
|
||||
|
||||
// Stable handler for opening docs
|
||||
const handleOpenDocs = useCallback(
|
||||
() => window.open('https://docs.sim.ai', '_blank', 'noopener,noreferrer'),
|
||||
[]
|
||||
)
|
||||
|
||||
// Stable blur handlers for inline rename inputs
|
||||
const handleTaskRenameBlur = useCallback(
|
||||
() => void taskFlyoutRename.saveRename(),
|
||||
[taskFlyoutRename.saveRename]
|
||||
)
|
||||
|
||||
const handleWorkflowRenameBlur = useCallback(
|
||||
() => void workflowFlyoutRename.saveRename(),
|
||||
[workflowFlyoutRename.saveRename]
|
||||
)
|
||||
|
||||
// Stable style for hidden file inputs
|
||||
const hiddenStyle = useMemo(() => ({ display: 'none' }) as const, [])
|
||||
|
||||
const resolveWorkspaceIdFromPath = useCallback((): string | undefined => {
|
||||
if (workspaceId) return workspaceId
|
||||
if (typeof window === 'undefined') return undefined
|
||||
@@ -1256,7 +1343,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
<div className='mt-2.5 flex flex-shrink-0 flex-col gap-0.5 px-2'>
|
||||
{topNavItems.map((item) => (
|
||||
<SidebarNavItem
|
||||
key={`${item.id}-${isCollapsed}`}
|
||||
key={item.id}
|
||||
item={item}
|
||||
active={item.href ? !!pathname?.startsWith(item.href) : false}
|
||||
showCollapsedTooltips={showCollapsedTooltips}
|
||||
@@ -1273,7 +1360,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
<div className='flex flex-col gap-0.5 px-2'>
|
||||
{workspaceNavItems.map((item) => (
|
||||
<SidebarNavItem
|
||||
key={`${item.id}-${isCollapsed}`}
|
||||
key={item.id}
|
||||
item={item}
|
||||
active={item.href ? !!pathname?.startsWith(item.href) : false}
|
||||
showCollapsedTooltips={showCollapsedTooltips}
|
||||
@@ -1302,7 +1389,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='h-[18px] w-[18px] rounded-sm p-0 hover-hover:bg-[var(--surface-active)]'
|
||||
onClick={() => navigateToPage(`/workspace/${workspaceId}/home`)}
|
||||
onClick={handleNewTask}
|
||||
>
|
||||
<Plus className='h-[16px] w-[16px]' />
|
||||
</Button>
|
||||
@@ -1316,16 +1403,11 @@ export const Sidebar = memo(function Sidebar() {
|
||||
</div>
|
||||
{isCollapsed ? (
|
||||
<CollapsedSidebarMenu
|
||||
icon={
|
||||
<Blimp className='h-[16px] w-[16px] flex-shrink-0 text-[var(--text-icon)]' />
|
||||
}
|
||||
icon={tasksCollapsedIcon}
|
||||
hover={tasksHover}
|
||||
ariaLabel='Tasks'
|
||||
className='mt-1.5'
|
||||
primaryAction={{
|
||||
label: 'New task',
|
||||
onSelect: () => navigateToPage(`/workspace/${workspaceId}/home`),
|
||||
}}
|
||||
primaryAction={tasksPrimaryAction}
|
||||
>
|
||||
{tasksLoading ? (
|
||||
<DropdownMenuItem disabled>
|
||||
@@ -1344,7 +1426,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
isRenaming={taskFlyoutRename.isSaving}
|
||||
onEditValueChange={taskFlyoutRename.setValue}
|
||||
onEditKeyDown={taskFlyoutRename.handleKeyDown}
|
||||
onEditBlur={() => void taskFlyoutRename.saveRename()}
|
||||
onEditBlur={handleTaskRenameBlur}
|
||||
onContextMenu={handleTaskContextMenu}
|
||||
onMorePointerDown={handleTaskMorePointerDown}
|
||||
onMoreClick={handleTaskMoreClick}
|
||||
@@ -1375,7 +1457,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
value={taskFlyoutRename.value}
|
||||
onChange={(e) => taskFlyoutRename.setValue(e.target.value)}
|
||||
onKeyDown={taskFlyoutRename.handleKeyDown}
|
||||
onBlur={() => void taskFlyoutRename.saveRename()}
|
||||
onBlur={handleTaskRenameBlur}
|
||||
className='min-w-0 flex-1 border-none bg-transparent font-base text-[14px] text-[var(--text-body)] outline-none'
|
||||
/>
|
||||
</div>
|
||||
@@ -1401,7 +1483,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
{tasks.length > visibleTaskCount && (
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setVisibleTaskCount((prev) => prev + 5)}
|
||||
onClick={handleSeeMoreTasks}
|
||||
className='mx-0.5 flex h-[30px] items-center gap-2 rounded-lg px-2 text-[var(--text-icon)] text-sm hover-hover:bg-[var(--surface-active)]'
|
||||
>
|
||||
<MoreHorizontal className='h-[16px] w-[16px] flex-shrink-0' />
|
||||
@@ -1485,23 +1567,11 @@ export const Sidebar = memo(function Sidebar() {
|
||||
</div>
|
||||
{isCollapsed ? (
|
||||
<CollapsedSidebarMenu
|
||||
icon={
|
||||
<div
|
||||
className='h-[16px] w-[16px] flex-shrink-0 rounded-[3px] border-[2px]'
|
||||
style={{
|
||||
backgroundColor: 'var(--text-icon)',
|
||||
borderColor: 'color-mix(in srgb, var(--text-icon) 60%, transparent)',
|
||||
backgroundClip: 'padding-box',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
icon={workflowsCollapsedIcon}
|
||||
hover={workflowsHover}
|
||||
ariaLabel='Workflows'
|
||||
className='mt-1.5'
|
||||
primaryAction={{
|
||||
label: 'New workflow',
|
||||
onSelect: handleCreateWorkflow,
|
||||
}}
|
||||
primaryAction={workflowsPrimaryAction}
|
||||
>
|
||||
{workflowsLoading && regularWorkflows.length === 0 ? (
|
||||
<DropdownMenuItem disabled>
|
||||
@@ -1523,7 +1593,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
isRenamingWorkflow={workflowFlyoutRename.isSaving}
|
||||
onEditValueChange={workflowFlyoutRename.setValue}
|
||||
onEditKeyDown={workflowFlyoutRename.handleKeyDown}
|
||||
onEditBlur={() => void workflowFlyoutRename.saveRename()}
|
||||
onEditBlur={handleWorkflowRenameBlur}
|
||||
onWorkflowContextMenu={handleCollapsedWorkflowContextMenu}
|
||||
onWorkflowMorePointerDown={handleCollapsedWorkflowMorePointerDown}
|
||||
onWorkflowMoreClick={handleCollapsedWorkflowMoreClick}
|
||||
@@ -1540,7 +1610,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
isRenaming={workflowFlyoutRename.isSaving}
|
||||
onEditValueChange={workflowFlyoutRename.setValue}
|
||||
onEditKeyDown={workflowFlyoutRename.handleKeyDown}
|
||||
onEditBlur={() => void workflowFlyoutRename.saveRename()}
|
||||
onEditBlur={handleWorkflowRenameBlur}
|
||||
onContextMenu={handleCollapsedWorkflowContextMenu}
|
||||
onMorePointerDown={handleCollapsedWorkflowMorePointerDown}
|
||||
onMoreClick={handleCollapsedWorkflowMoreClick}
|
||||
@@ -1601,15 +1671,11 @@ export const Sidebar = memo(function Sidebar() {
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
<DropdownMenuContent align='start' side='top' sideOffset={4}>
|
||||
<DropdownMenuItem
|
||||
onSelect={() =>
|
||||
window.open('https://docs.sim.ai', '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
>
|
||||
<DropdownMenuItem onSelect={handleOpenDocs}>
|
||||
<BookOpen className='h-[14px] w-[14px]' />
|
||||
Docs
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => setIsHelpModalOpen(true)}>
|
||||
<DropdownMenuItem onSelect={handleOpenHelpFromMenu}>
|
||||
<HelpCircle className='h-[14px] w-[14px]' />
|
||||
Report an issue
|
||||
</DropdownMenuItem>
|
||||
@@ -1622,7 +1688,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
|
||||
{footerItems.map((item) => (
|
||||
<SidebarNavItem
|
||||
key={`${item.id}-${isCollapsed}`}
|
||||
key={item.id}
|
||||
item={item}
|
||||
active={false}
|
||||
showCollapsedTooltips={showCollapsedTooltips}
|
||||
@@ -1673,7 +1739,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
onClose={closeCollapsedWorkflowContextMenu}
|
||||
onOpenInNewTab={handleCollapsedWorkflowOpenInNewTab}
|
||||
onRename={handleStartCollapsedWorkflowRename}
|
||||
onDelete={() => {}}
|
||||
onDelete={noop}
|
||||
showOpenInNewTab={true}
|
||||
showRename={true}
|
||||
showDuplicate={false}
|
||||
@@ -1685,7 +1751,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
{/* Task Delete Confirmation Modal */}
|
||||
<DeleteModal
|
||||
isOpen={isTaskDeleteModalOpen}
|
||||
onClose={() => setIsTaskDeleteModalOpen(false)}
|
||||
onClose={handleCloseTaskDeleteModal}
|
||||
onConfirm={handleConfirmDeleteTasks}
|
||||
isDeleting={deleteTaskMutation.isPending || deleteTasksMutation.isPending}
|
||||
itemType='task'
|
||||
@@ -1732,7 +1798,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
ref={workspaceFileInputRef}
|
||||
type='file'
|
||||
accept='.zip'
|
||||
style={{ display: 'none' }}
|
||||
style={hiddenStyle}
|
||||
onChange={handleWorkspaceFileChange}
|
||||
/>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user