improvement(resources): all outer page structure complete

This commit is contained in:
Emir Karabeg
2026-03-07 14:42:11 -08:00
parent 8ff93fe842
commit de32644940
10 changed files with 404 additions and 308 deletions

View File

@@ -1,8 +1,8 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import type { PostHog } from 'posthog-js'
import { createLogger } from '@sim/logger'
import type { PostHog } from 'posthog-js'
import { getEnv, isTruthy } from '@/lib/core/config/env'
const logger = createLogger('PostHogProvider')

View File

@@ -78,22 +78,24 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
if (isPending) return
import('posthog-js').then(({ default: posthog }) => {
try {
if (typeof posthog.identify !== 'function') return
import('posthog-js')
.then(({ default: posthog }) => {
try {
if (typeof posthog.identify !== 'function') return
if (data?.user) {
posthog.identify(data.user.id, {
email: data.user.email,
name: data.user.name,
email_verified: data.user.emailVerified,
created_at: data.user.createdAt,
})
} else {
posthog.reset()
}
} catch {}
}).catch(() => {})
if (data?.user) {
posthog.identify(data.user.id, {
email: data.user.email,
name: data.user.name,
email_verified: data.user.emailVerified,
created_at: data.user.createdAt,
})
} else {
posthog.reset()
}
} catch {}
})
.catch(() => {})
}, [data, isPending])
const value = useMemo<SessionHookResult>(

View File

@@ -111,14 +111,14 @@ export function Resource({
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
<div className='flex items-center justify-between'>
{search && (
<div className='relative'>
<div className='relative flex-1'>
<Search className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-0 h-[14px] w-[14px] text-[var(--text-muted)]' />
<input
type='text'
value={search.value}
onChange={(e) => search.onChange(e.target.value)}
placeholder={search.placeholder ?? 'Search...'}
className='bg-transparent py-[4px] pl-[24px] font-base text-[12px] text-[var(--text-secondary)] outline-none placeholder:text-[var(--text-subtle)]'
className='w-full bg-transparent py-[4px] pl-[24px] font-base text-[12px] text-[var(--text-secondary)] outline-none placeholder:text-[var(--text-subtle)]'
/>
</div>
)}
@@ -223,9 +223,9 @@ function EmptyMessage({ title, description }: { title: string; description?: str
function CellContent({ cell }: { cell: ResourceCell }) {
return (
<span className='flex items-center gap-[12px] font-medium text-[14px] text-[var(--text-secondary)]'>
{cell.icon && <span className='text-[var(--text-subtle)]'>{cell.icon}</span>}
<span>{cell.label}</span>
<span className='flex min-w-0 items-center gap-[12px] font-medium text-[14px] text-[var(--text-secondary)]'>
{cell.icon && <span className='flex-shrink-0 text-[var(--text-subtle)]'>{cell.icon}</span>}
<span className='truncate'>{cell.label}</span>
</span>
)
}

View File

@@ -2,33 +2,21 @@
import { useCallback, useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
import { ChevronDown, Database, Search } from 'lucide-react'
import { useParams } from 'next/navigation'
import {
Button,
Popover,
PopoverContent,
PopoverItem,
PopoverTrigger,
Tooltip,
} from '@/components/emcn'
import { Input } from '@/components/ui/input'
import { Database } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { formatRelativeTime } from '@/lib/core/utils/formatting'
import type { KnowledgeBaseData } from '@/lib/knowledge/types'
import type { ResourceColumn, ResourceRow } from '@/app/workspace/[workspaceId]/components'
import { Resource } from '@/app/workspace/[workspaceId]/components'
import { BaseTagsModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/components'
import {
BaseCard,
BaseCardSkeletonGrid,
CreateBaseModal,
DeleteKnowledgeBaseModal,
EditKnowledgeBaseModal,
KnowledgeBaseContextMenu,
KnowledgeListContextMenu,
} from '@/app/workspace/[workspaceId]/knowledge/components'
import {
SORT_OPTIONS,
type SortOption,
type SortOrder,
} from '@/app/workspace/[workspaceId]/knowledge/components/constants'
import {
filterKnowledgeBases,
sortKnowledgeBases,
} from '@/app/workspace/[workspaceId]/knowledge/utils/sort'
import { filterKnowledgeBases } from '@/app/workspace/[workspaceId]/knowledge/utils/sort'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
import { useKnowledgeBasesList } from '@/hooks/kb/use-knowledge'
@@ -37,19 +25,21 @@ import { useDebounce } from '@/hooks/use-debounce'
const logger = createLogger('Knowledge')
/**
* Extended knowledge base data with document count
*/
interface KnowledgeBaseWithDocCount extends KnowledgeBaseData {
docCount?: number
}
/**
* Knowledge base list component displaying all knowledge bases in a workspace
* Supports filtering by search query and sorting options
*/
const COLUMNS: ResourceColumn[] = [
{ id: 'name', header: 'Name', width: 'w-[35%]' },
{ id: 'documents', header: 'Documents', width: 'w-[12%]' },
{ id: 'description', header: 'Description', width: 'w-[28%]' },
{ id: 'updated', header: 'Updated', width: 'w-[13%]' },
{ id: 'id', header: 'ID', width: 'w-[12%]' },
]
export function Knowledge() {
const params = useParams()
const router = useRouter()
const workspaceId = params.workspaceId as string
const { knowledgeBases, isLoading, error } = useKnowledgeBasesList(workspaceId)
@@ -61,9 +51,14 @@ export function Knowledge() {
const [searchQuery, setSearchQuery] = useState('')
const debouncedSearchQuery = useDebounce(searchQuery, 300)
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
const [isSortPopoverOpen, setIsSortPopoverOpen] = useState(false)
const [sortBy, setSortBy] = useState<SortOption>('updatedAt')
const [sortOrder, setSortOrder] = useState<SortOrder>('desc')
const [activeKnowledgeBase, setActiveKnowledgeBase] = useState<KnowledgeBaseWithDocCount | null>(
null
)
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const [isTagsModalOpen, setIsTagsModalOpen] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const {
isOpen: isListContextMenuOpen,
@@ -73,46 +68,36 @@ export function Knowledge() {
closeMenu: closeListContextMenu,
} = useContextMenu()
/**
* Handle context menu on the content area - only show menu when clicking on empty space
*/
const {
isOpen: isRowContextMenuOpen,
position: rowContextMenuPosition,
menuRef: rowMenuRef,
handleContextMenu: handleRowCtxMenu,
closeMenu: closeRowContextMenu,
} = useContextMenu()
const handleContentContextMenu = useCallback(
(e: React.MouseEvent) => {
const target = e.target as HTMLElement
const isOnCard = target.closest('[data-kb-card]')
const isOnInteractive = target.closest('button, input, a, [role="button"]')
if (!isOnCard && !isOnInteractive) {
handleListContextMenu(e)
if (
target.closest('[data-resource-row]') ||
target.closest('button, input, a, [role="button"]')
) {
return
}
handleListContextMenu(e)
},
[handleListContextMenu]
)
/**
* Handle add knowledge base from context menu
*/
const handleAddKnowledgeBase = useCallback(() => {
setIsCreateModalOpen(true)
}, [])
const currentSortValue = `${sortBy}-${sortOrder}`
const currentSortLabel =
SORT_OPTIONS.find((opt) => opt.value === currentSortValue)?.label || 'Last Updated'
const handleSort = useCallback(() => {}, [])
/**
* Handles sort option change from dropdown
*/
const handleSortChange = (value: string) => {
const [field, order] = value.split('-') as [SortOption, SortOrder]
setSortBy(field)
setSortOrder(order)
setIsSortPopoverOpen(false)
}
const handleFilter = useCallback(() => {}, [])
/**
* Updates a knowledge base name and description
*/
const handleUpdateKnowledgeBase = useCallback(
async (id: string, name: string, description: string) => {
await updateKnowledgeBaseMutation({
@@ -124,9 +109,6 @@ export function Knowledge() {
[updateKnowledgeBaseMutation]
)
/**
* Deletes a knowledge base
*/
const handleDeleteKnowledgeBase = useCallback(
async (id: string) => {
await deleteKnowledgeBaseMutation({ knowledgeBaseId: id })
@@ -135,30 +117,72 @@ export function Knowledge() {
[deleteKnowledgeBaseMutation]
)
/**
* Filter and sort knowledge bases based on search query and sort options
*/
const filteredAndSortedKnowledgeBases = useMemo(() => {
const filtered = filterKnowledgeBases(knowledgeBases, debouncedSearchQuery)
return sortKnowledgeBases(filtered, sortBy, sortOrder)
}, [knowledgeBases, debouncedSearchQuery, sortBy, sortOrder])
const filteredKnowledgeBases = useMemo(
() => filterKnowledgeBases(knowledgeBases, debouncedSearchQuery),
[knowledgeBases, debouncedSearchQuery]
)
/**
* Format knowledge base data for display in the card
*/
const formatKnowledgeBaseForDisplay = (kb: KnowledgeBaseWithDocCount) => ({
id: kb.id,
title: kb.name,
docCount: kb.docCount || 0,
description: kb.description || 'No description provided',
createdAt: kb.createdAt,
updatedAt: kb.updatedAt,
connectorTypes: kb.connectorTypes ?? [],
})
const rows: ResourceRow[] = useMemo(
() =>
filteredKnowledgeBases.map((kb) => {
const kbWithCount = kb as KnowledgeBaseWithDocCount
return {
id: kb.id,
cells: {
name: {
icon: <Database className='h-[14px] w-[14px]' />,
label: kb.name,
},
documents: {
label: String(kbWithCount.docCount || 0),
},
description: {
label: kb.description || 'No description',
},
updated: {
label: kb.updatedAt ? formatRelativeTime(kb.updatedAt) : '',
},
id: {
label: `kb-${kb.id.slice(0, 8)}`,
},
},
}
}),
[filteredKnowledgeBases]
)
const handleRowClick = useCallback(
(rowId: string) => {
if (isRowContextMenuOpen) return
const kb = knowledgeBases.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]
)
const handleRowContextMenu = useCallback(
(e: React.MouseEvent, rowId: string) => {
const kb = knowledgeBases.find((k) => k.id === rowId) as KnowledgeBaseWithDocCount | undefined
setActiveKnowledgeBase(kb ?? null)
handleRowCtxMenu(e)
},
[knowledgeBases, handleRowCtxMenu]
)
const handleConfirmDelete = useCallback(async () => {
if (!activeKnowledgeBase) return
setIsDeleting(true)
try {
await handleDeleteKnowledgeBase(activeKnowledgeBase.id)
setIsDeleteModalOpen(false)
setActiveKnowledgeBase(null)
} finally {
setIsDeleting(false)
}
}, [activeKnowledgeBase, handleDeleteKnowledgeBase])
/**
* Get empty state content based on current filters
*/
const emptyState = useMemo(() => {
if (debouncedSearchQuery) {
return {
@@ -166,7 +190,6 @@ export function Knowledge() {
description: 'Try a different search term',
}
}
return {
title: 'No knowledge bases yet',
description:
@@ -178,125 +201,37 @@ export function Knowledge() {
return (
<>
<div className='flex h-full flex-1 flex-col'>
<div className='flex flex-1 overflow-hidden'>
<div
className='flex flex-1 flex-col overflow-auto bg-white px-[24px] pt-[28px] pb-[24px] dark:bg-[var(--bg)]'
onContextMenu={handleContentContextMenu}
>
<div>
<div className='flex items-start gap-[12px]'>
<div className='flex h-[26px] w-[26px] items-center justify-center rounded-[6px] border border-[#5BB377] bg-[#E8F7EE] dark:border-[#1E5A3E] dark:bg-[#0F3D2C]'>
<Database className='h-[14px] w-[14px] text-[#5BB377] dark:text-[#34D399]' />
</div>
<h1 className='font-medium text-[18px]'>Knowledge Base</h1>
</div>
<p className='mt-[10px] text-[14px] text-[var(--text-tertiary)]'>
Create and manage knowledge bases with custom files.
</p>
</div>
<div className='mt-[14px] flex items-center justify-between'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-4)] px-[8px]'>
<Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' />
<Input
placeholder='Search'
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className='flex-1 border-0 bg-transparent px-0 font-medium text-[var(--text-secondary)] text-small leading-none placeholder:text-[var(--text-subtle)] focus-visible:ring-0 focus-visible:ring-offset-0'
/>
</div>
<div className='flex items-center gap-[8px]'>
{knowledgeBases.length > 0 && (
<Popover open={isSortPopoverOpen} onOpenChange={setIsSortPopoverOpen}>
<PopoverTrigger asChild>
<Button variant='default' className='h-[32px] rounded-[6px]'>
{currentSortLabel}
<ChevronDown className='ml-2 h-4 w-4 text-muted-foreground' />
</Button>
</PopoverTrigger>
<PopoverContent align='end' side='bottom' sideOffset={4}>
<div className='flex flex-col gap-[2px]'>
{SORT_OPTIONS.map((option) => (
<PopoverItem
key={option.value}
active={currentSortValue === option.value}
onClick={() => handleSortChange(option.value)}
>
{option.label}
</PopoverItem>
))}
</div>
</PopoverContent>
</Popover>
)}
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
onClick={() => setIsCreateModalOpen(true)}
disabled={userPermissions.canEdit !== true}
variant='tertiary'
className='h-[32px] rounded-[6px]'
>
Create
</Button>
</Tooltip.Trigger>
{userPermissions.canEdit !== true && (
<Tooltip.Content>
Write permission required to create knowledge bases
</Tooltip.Content>
)}
</Tooltip.Root>
</div>
</div>
<div className='mt-[24px] grid grid-cols-1 gap-[20px] md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'>
{isLoading ? (
<BaseCardSkeletonGrid count={8} />
) : filteredAndSortedKnowledgeBases.length === 0 ? (
<div className='col-span-full flex h-64 items-center justify-center rounded-lg border border-muted-foreground/25 bg-muted/20'>
<div className='text-center'>
<p className='font-medium text-[var(--text-secondary)] text-sm'>
{emptyState.title}
</p>
<p className='mt-1 text-[var(--text-muted)] text-xs'>
{emptyState.description}
</p>
</div>
</div>
) : error ? (
<div className='col-span-full flex h-64 items-center justify-center rounded-lg border border-muted-foreground/25 bg-muted/20'>
<div className='text-center'>
<p className='font-medium text-[var(--text-secondary)] text-sm'>
Error loading knowledge bases
</p>
<p className='mt-1 text-[var(--text-muted)] text-xs'>{error}</p>
</div>
</div>
) : (
filteredAndSortedKnowledgeBases.map((kb) => {
const displayData = formatKnowledgeBaseForDisplay(kb as KnowledgeBaseWithDocCount)
return (
<BaseCard
key={kb.id}
id={displayData.id}
title={displayData.title}
docCount={displayData.docCount}
description={displayData.description}
connectorTypes={displayData.connectorTypes}
createdAt={displayData.createdAt}
updatedAt={displayData.updatedAt}
onUpdate={handleUpdateKnowledgeBase}
onDelete={handleDeleteKnowledgeBase}
/>
)
})
)}
</div>
</div>
</div>
</div>
<Resource
icon={Database}
title='Knowledge Base'
create={{
label: 'Create',
onClick: () => setIsCreateModalOpen(true),
disabled: userPermissions.canEdit !== true,
}}
search={{
value: searchQuery,
onChange: setSearchQuery,
placeholder: 'Search knowledge bases...',
}}
onSort={handleSort}
onFilter={handleFilter}
columns={COLUMNS}
rows={rows}
onRowClick={handleRowClick}
onRowContextMenu={handleRowContextMenu}
isLoading={isLoading}
error={
error
? {
title: 'Error loading knowledge bases',
description: error,
}
: undefined
}
emptyState={emptyState}
onContextMenu={handleContentContextMenu}
/>
<KnowledgeListContextMenu
isOpen={isListContextMenuOpen}
@@ -307,6 +242,64 @@ export function Knowledge() {
disableAdd={userPermissions.canEdit !== true}
/>
{activeKnowledgeBase && (
<KnowledgeBaseContextMenu
isOpen={isRowContextMenuOpen}
position={rowContextMenuPosition}
menuRef={rowMenuRef}
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)}
showOpenInNewTab
showViewTags
showEdit
showDelete
disableEdit={!userPermissions.canEdit}
disableDelete={!userPermissions.canEdit}
/>
)}
{activeKnowledgeBase && (
<EditKnowledgeBaseModal
open={isEditModalOpen}
onOpenChange={setIsEditModalOpen}
knowledgeBaseId={activeKnowledgeBase.id}
initialName={activeKnowledgeBase.name}
initialDescription={activeKnowledgeBase.description || ''}
onSave={handleUpdateKnowledgeBase}
/>
)}
{activeKnowledgeBase && (
<DeleteKnowledgeBaseModal
isOpen={isDeleteModalOpen}
onClose={() => {
setIsDeleteModalOpen(false)
setActiveKnowledgeBase(null)
}}
onConfirm={handleConfirmDelete}
isDeleting={isDeleting}
knowledgeBaseName={activeKnowledgeBase.name}
/>
)}
{activeKnowledgeBase && (
<BaseTagsModal
open={isTagsModalOpen}
onOpenChange={setIsTagsModalOpen}
knowledgeBaseId={activeKnowledgeBase.id}
/>
)}
<CreateBaseModal open={isCreateModalOpen} onOpenChange={setIsCreateModalOpen} />
</>
)

View File

@@ -1,29 +1,133 @@
'use client'
import { Calendar } from 'lucide-react'
import { ScheduleList } from '@/app/workspace/[workspaceId]/schedules/components'
import { useCallback, useMemo, useState } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { Calendar, MoreHorizontal } from '@/components/emcn/icons'
import { formatAbsoluteDate, formatRelativeTime } from '@/lib/core/utils/formatting'
import { parseCronToHumanReadable } from '@/lib/workflows/schedules/utils'
import type { ResourceColumn, ResourceRow } from '@/app/workspace/[workspaceId]/components'
import { Resource } from '@/app/workspace/[workspaceId]/components'
import type { WorkspaceScheduleData } from '@/hooks/queries/schedules'
import { useWorkspaceSchedules } from '@/hooks/queries/schedules'
import { useDebounce } from '@/hooks/use-debounce'
function getHumanReadable(s: WorkspaceScheduleData) {
if (!s.cronExpression && s.nextRunAt) return `Once at ${formatAbsoluteDate(s.nextRunAt)}`
if (s.cronExpression) return parseCronToHumanReadable(s.cronExpression, s.timezone)
return 'Unknown schedule'
}
const COLUMNS: ResourceColumn[] = [
{ id: 'name', header: 'Name', width: 'w-[25%]' },
{ id: 'type', header: 'Type', width: 'w-[13%]' },
{ id: 'schedule', header: 'Schedule', width: 'w-[24%]' },
{ id: 'status', header: 'Status', width: 'w-[10%]' },
{ id: 'nextRun', header: 'Next Run', width: 'w-[18%]' },
{ id: 'actions', header: 'Actions', width: 'w-[10%]' },
]
export function Schedules() {
return (
<div className='flex h-full flex-1 flex-col'>
<div className='flex flex-1 overflow-hidden'>
<div className='flex flex-1 flex-col overflow-auto bg-white px-[24px] pt-[28px] pb-[24px] dark:bg-[var(--bg)]'>
{/* Header */}
<div>
<div className='flex items-start gap-[12px]'>
<div className='flex h-[26px] w-[26px] items-center justify-center rounded-[6px] border border-[#F59E0B] bg-[#FFFBEB] dark:border-[#B45309] dark:bg-[#451A03]'>
<Calendar className='h-[14px] w-[14px] text-[#F59E0B] dark:text-[#FBBF24]' />
</div>
<h1 className='font-medium text-[18px]'>Schedules</h1>
</div>
<p className='mt-[10px] text-[14px] text-[var(--text-tertiary)]'>
View all scheduled workflows and jobs in your workspace.
</p>
</div>
const params = useParams()
const router = useRouter()
const workspaceId = params.workspaceId as string
<ScheduleList />
</div>
</div>
</div>
const { data: allItems = [], isLoading, error } = useWorkspaceSchedules(workspaceId)
const [searchQuery, setSearchQuery] = useState('')
const debouncedSearchQuery = useDebounce(searchQuery, 300)
const visibleItems = useMemo(
() => allItems.filter((item) => item.status !== 'completed'),
[allItems]
)
const filteredItems = useMemo(() => {
if (!debouncedSearchQuery) return visibleItems
const q = debouncedSearchQuery.toLowerCase()
return visibleItems.filter((item) => {
const name =
item.sourceType === 'job'
? item.jobTitle || item.sourceTaskName || ''
: item.workflowName || ''
return name.toLowerCase().includes(q) || getHumanReadable(item).toLowerCase().includes(q)
})
}, [visibleItems, debouncedSearchQuery])
const rows: ResourceRow[] = useMemo(
() =>
filteredItems.map((item) => {
const isJob = item.sourceType === 'job'
const name = isJob ? item.jobTitle || item.sourceTaskName || '—' : item.workflowName || '—'
return {
id: item.id,
cells: {
name: {
icon: <Calendar className='h-[14px] w-[14px]' />,
label: name,
},
type: { label: isJob ? 'Scheduled Task' : 'Workflow' },
schedule: { label: getHumanReadable(item) },
status: { label: item.status },
nextRun: { label: item.nextRunAt ? formatRelativeTime(item.nextRunAt) : '—' },
actions: {
icon: <MoreHorizontal className='h-[14px] w-[14px]' />,
label: '',
},
},
}
}),
[filteredItems]
)
const handleRowClick = useCallback(
(rowId: string) => {
const item = filteredItems.find((i) => i.id === rowId)
if (item?.workflowId) {
router.push(`/workspace/${workspaceId}/w/${item.workflowId}`)
}
},
[filteredItems, router, workspaceId]
)
const emptyState = useMemo(() => {
if (debouncedSearchQuery) {
return { title: 'No schedules found', description: 'Try a different search term' }
}
return {
title: 'No schedules yet',
description: 'Scheduled workflows and tasks will appear here',
}
}, [debouncedSearchQuery])
return (
<Resource
icon={Calendar}
title='Schedules'
create={{
label: 'Create',
onClick: () => {},
}}
search={{
value: searchQuery,
onChange: setSearchQuery,
placeholder: 'Search schedules...',
}}
onSort={() => {}}
onFilter={() => {}}
columns={COLUMNS}
rows={rows}
onRowClick={handleRowClick}
isLoading={isLoading}
error={
error
? {
title: 'Error loading schedules',
description: error instanceof Error ? error.message : 'An error occurred',
}
: undefined
}
emptyState={emptyState}
/>
)
}

View File

@@ -41,10 +41,6 @@ export function Tables() {
const [isSchemaModalOpen, setIsSchemaModalOpen] = useState(false)
const [activeTable, setActiveTable] = useState<TableDefinition | null>(null)
const [searchTerm, setSearchTerm] = useState('')
const [sortField, setSortField] = useState<string | null>(null)
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
const [filterField, setFilterField] = useState<string | null>(null)
const [filterValue, setFilterValue] = useState<string | null>(null)
const {
isOpen: isListContextMenuOpen,
@@ -96,15 +92,9 @@ export function Tables() {
[filteredTables]
)
const handleSort = useCallback(() => {
setSortDirection((prev) => (prev === 'asc' ? 'desc' : 'asc'))
setSortField((prev) => prev ?? 'name')
}, [])
const handleSort = useCallback(() => {}, [])
const handleFilter = useCallback(() => {
setFilterField((prev) => (prev ? null : 'name'))
setFilterValue(null)
}, [])
const handleFilter = useCallback(() => {}, [])
const handleContentContextMenu = useCallback(
(e: React.MouseEvent) => {

View File

@@ -2,11 +2,11 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Command } from 'cmdk'
import { Clock, Database, Files, HelpCircle, Layout, Settings } from 'lucide-react'
import { Database, Files, HelpCircle, Settings } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { createPortal } from 'react-dom'
import { Blimp, Library } from '@/components/emcn'
import { Table } from '@/components/emcn/icons'
import { Calendar, Table } from '@/components/emcn/icons'
import { cn } from '@/lib/core/utils/cn'
import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils'
import { SIDEBAR_SCROLL_EVENT } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar'
@@ -107,27 +107,6 @@ export function SearchModal({
const pages = useMemo(
(): PageItem[] =>
[
{
id: 'logs',
name: 'Logs',
icon: Library,
href: `/workspace/${workspaceId}/logs`,
shortcut: '⌘⇧L',
},
{
id: 'templates',
name: 'Templates',
icon: Layout,
href: `/workspace/${workspaceId}/templates`,
hidden: permissionConfig.hideTemplates,
},
{
id: 'knowledge-base',
name: 'Knowledge Base',
icon: Database,
href: `/workspace/${workspaceId}/knowledge`,
hidden: permissionConfig.hideKnowledgeBaseTab,
},
{
id: 'tables',
name: 'Tables',
@@ -142,12 +121,26 @@ export function SearchModal({
href: `/workspace/${workspaceId}/files`,
hidden: permissionConfig.hideFilesTab,
},
{
id: 'knowledge-base',
name: 'Knowledge Base',
icon: Database,
href: `/workspace/${workspaceId}/knowledge`,
hidden: permissionConfig.hideKnowledgeBaseTab,
},
{
id: 'schedules',
name: 'Schedules',
icon: Clock,
icon: Calendar,
href: `/workspace/${workspaceId}/schedules`,
},
{
id: 'logs',
name: 'Logs',
icon: Library,
href: `/workspace/${workspaceId}/logs`,
shortcut: '⌘⇧L',
},
{
id: 'help',
name: 'Help',
@@ -165,8 +158,8 @@ export function SearchModal({
workspaceId,
openHelpModal,
navigateToSettings,
permissionConfig.hideTemplates,
permissionConfig.hideKnowledgeBaseTab,
permissionConfig.hideTablesTab,
permissionConfig.hideFilesTab,
]
)

View File

@@ -322,7 +322,7 @@ export const Sidebar = memo(function Sidebar() {
},
{
id: 'schedules',
label: 'Scheduled Tasks',
label: 'Schedules',
icon: Calendar,
href: `/workspace/${workspaceId}/schedules`,
},
@@ -764,8 +764,26 @@ export const Sidebar = memo(function Sidebar() {
>
{/* Tasks */}
<div className='flex flex-shrink-0 flex-col'>
<div className='px-[16px]'>
<div className='font-base text-[var(--text-icon)] text-small'>All tasks</div>
<div className='flex flex-shrink-0 flex-col space-y-[4px] px-[16px]'>
<div className='flex items-center justify-between'>
<div className='font-base text-[var(--text-icon)] text-small'>All tasks</div>
<div className='flex items-center justify-center gap-[8px]'>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='ghost'
className='h-[18px] w-[18px] rounded-[4px] p-0 hover:bg-[var(--surface-active)]'
onClick={() => router.push(`/workspace/${workspaceId}/home`)}
>
<Plus className='h-[16px] w-[16px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content>
<p>New task</p>
</Tooltip.Content>
</Tooltip.Root>
</div>
</div>
</div>
<div className='mt-[6px] flex flex-col gap-[2px] px-[8px]'>
{tasksLoading ? (
@@ -870,7 +888,7 @@ export const Sidebar = memo(function Sidebar() {
</Button>
</Tooltip.Trigger>
<Tooltip.Content>
<p>{isCreatingWorkflow ? 'Creating workflow...' : 'Create workflow'}</p>
<p>{isCreatingWorkflow ? 'Creating workflow...' : 'New workflow'}</p>
</Tooltip.Content>
</Tooltip.Root>
</div>

View File

@@ -15,7 +15,7 @@ const Switch = React.forwardRef<
<SwitchPrimitives.Root
disabled={disabled}
className={cn(
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full bg-[var(--border-1)] transition-colors focus-visible:outline-none data-[disabled]:cursor-not-allowed data-[state=checked]:bg-[var(--brand-tertiary-2)] data-[disabled]:opacity-50',
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full bg-[var(--border-1)] transition-colors focus-visible:outline-none data-[disabled]:cursor-not-allowed data-[state=checked]:bg-[var(--c-2A2A2A)] data-[disabled]:opacity-50',
className
)}
{...props}

View File

@@ -578,9 +578,7 @@ export function useUndoRedo() {
const { edgeSnapshots } = batchAddInverse.data
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
const edgesToAdd = edgeSnapshots.filter(
(e) => !existingEdgeIds.has(e.id)
)
const edgesToAdd = edgeSnapshots.filter((e) => !existingEdgeIds.has(e.id))
if (edgesToAdd.length > 0) {
addToQueue({
@@ -633,10 +631,10 @@ export function useUndoRedo() {
if (useWorkflowStore.getState().blocks[blockId]) {
if (newParentId && affectedEdges && affectedEdges.length > 0) {
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
const edgesToAdd = affectedEdges.filter(
(e) => !existingEdgeIds.has(e.id)
const existingEdgeIds = new Set(
useWorkflowStore.getState().edges.map((edge) => edge.id)
)
const edgesToAdd = affectedEdges.filter((e) => !existingEdgeIds.has(e.id))
if (edgesToAdd.length > 0) {
addToQueue({
id: crypto.randomUUID(),
@@ -698,7 +696,9 @@ export function useUndoRedo() {
// If we're removing FROM a subflow (undo of add to subflow), remove edges after
if (!newParentId && affectedEdges && affectedEdges.length > 0) {
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
const existingEdgeIds = new Set(
useWorkflowStore.getState().edges.map((edge) => edge.id)
)
const edgeIdsToRemove = affectedEdges
.filter((edge) => existingEdgeIds.has(edge.id))
.map((edge) => edge.id)
@@ -743,9 +743,7 @@ export function useUndoRedo() {
// Moving OUT of subflow (undoing insert) → restore edges first
if (!newParentId && affectedEdges && affectedEdges.length > 0) {
const edgesToAdd = affectedEdges.filter(
(e) => !existingEdgeIds.has(e.id)
)
const edgesToAdd = affectedEdges.filter((e) => !existingEdgeIds.has(e.id))
allEdgesToAdd.push(...edgesToAdd)
}
@@ -1198,9 +1196,7 @@ export function useUndoRedo() {
const { edgeSnapshots } = batchAddOp.data
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
const edgesToAdd = edgeSnapshots.filter(
(e) => !existingEdgeIds.has(e.id)
)
const edgesToAdd = edgeSnapshots.filter((e) => !existingEdgeIds.has(e.id))
if (edgesToAdd.length > 0) {
addToQueue({
@@ -1256,7 +1252,9 @@ export function useUndoRedo() {
if (useWorkflowStore.getState().blocks[blockId]) {
// If we're removing FROM a subflow, remove edges first
if (!newParentId && affectedEdges && affectedEdges.length > 0) {
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
const existingEdgeIds = new Set(
useWorkflowStore.getState().edges.map((edge) => edge.id)
)
const edgeIdsToRemove = affectedEdges
.filter((edge) => existingEdgeIds.has(edge.id))
.map((edge) => edge.id)
@@ -1324,10 +1322,10 @@ export function useUndoRedo() {
// If we're adding TO a subflow, restore edges after
if (newParentId && affectedEdges && affectedEdges.length > 0) {
const existingEdgeIds = new Set(useWorkflowStore.getState().edges.map((edge) => edge.id))
const edgesToAdd = affectedEdges.filter(
(e) => !existingEdgeIds.has(e.id)
const existingEdgeIds = new Set(
useWorkflowStore.getState().edges.map((edge) => edge.id)
)
const edgesToAdd = affectedEdges.filter((e) => !existingEdgeIds.has(e.id))
if (edgesToAdd.length > 0) {
addToQueue({
id: crypto.randomUUID(),
@@ -1375,9 +1373,7 @@ export function useUndoRedo() {
// Moving OUT of subflow (redoing removal) → restore edges after
if (!newParentId && affectedEdges && affectedEdges.length > 0) {
const edgesToAdd = affectedEdges.filter(
(e) => !existingEdgeIds.has(e.id)
)
const edgesToAdd = affectedEdges.filter((e) => !existingEdgeIds.has(e.id))
allEdgesToAdd.push(...edgesToAdd)
}
}