From e622b6e87cff1aac1d49e5a00365d1c831c0fc65 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 17:52:48 -0700 Subject: [PATCH 01/30] improvement(tables): improve table filtering UX - Replace popover filter with persistent inline panel below toolbar - Add AND/OR toggle between filter rules (shown in Where label slot) - Sync filter panel state from applied filter on open - Show filter button active state when filter is applied or panel is open - Use readable operator labels matching dropdown options - Add Clear filters button (shown only when filter is active) - Close filter panel when last rule is removed via X - Fix empty gap rows appearing in filtered results by skipping position gap rendering when filter is active - Add toggle mode to ResourceOptionsBar for inline panel pattern - Memoize FilterRuleRow for perf, fix filterTags key collision, remove dead filterActiveCount prop --- .../resource-options-bar.tsx | 64 ++++-- .../components/table-filter/table-filter.tsx | 168 ++++++++++------ .../[tableId]/components/table/table.tsx | 187 ++++++++++-------- 3 files changed, 261 insertions(+), 158 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx index d16f83dc4fe..3d788e1aac5 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx @@ -19,8 +19,6 @@ import { cn } from '@/lib/core/utils/cn' const SEARCH_ICON = ( ) -const FILTER_ICON = -const SORT_ICON = type SortDirection = 'asc' | 'desc' @@ -67,7 +65,12 @@ export interface SearchConfig { interface ResourceOptionsBarProps { search?: SearchConfig sort?: SortConfig + /** Popover content — renders inside a Popover (used by logs, etc.) */ filter?: ReactNode + /** When provided, Filter button acts as a toggle instead of opening a Popover */ + onFilterToggle?: () => void + /** Whether the filter is currently active (highlights the toggle button) */ + filterActive?: boolean filterTags?: FilterTag[] extras?: ReactNode } @@ -76,10 +79,13 @@ export const ResourceOptionsBar = memo(function ResourceOptionsBar({ search, sort, filter, + onFilterToggle, + filterActive, filterTags, extras, }: ResourceOptionsBarProps) { - const hasContent = search || sort || filter || extras || (filterTags && filterTags.length > 0) + const hasContent = + search || sort || filter || onFilterToggle || extras || (filterTags && filterTags.length > 0) if (!hasContent) return null return ( @@ -88,22 +94,39 @@ export const ResourceOptionsBar = memo(function ResourceOptionsBar({ {search && }
{extras} - {filterTags?.map((tag) => ( + {filterTags?.map((tag, i) => ( ))} - {filter && ( + {onFilterToggle ? ( + + ) : filter ? ( @@ -111,15 +134,13 @@ export const ResourceOptionsBar = memo(function ResourceOptionsBar({ {filter} - )} + ) : null} {sort && }
@@ -213,8 +234,19 @@ const SortDropdown = memo(function SortDropdown({ config }: { config: SortConfig return ( - diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx index 0e748546771..43e3d16d657 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useMemo, useState } from 'react' +import { memo, useCallback, useMemo, useState } from 'react' import { X } from 'lucide-react' import { nanoid } from 'nanoid' import { @@ -11,29 +11,26 @@ import { DropdownMenuTrigger, } from '@/components/emcn' import { ChevronDown, Plus } from '@/components/emcn/icons' -import { cn } from '@/lib/core/utils/cn' import type { Filter, FilterRule } from '@/lib/table' import { COMPARISON_OPERATORS } from '@/lib/table/query-builder/constants' -import { filterRulesToFilter } from '@/lib/table/query-builder/converters' - -const OPERATOR_LABELS: Record = { - eq: '=', - ne: '≠', - gt: '>', - gte: '≥', - lt: '<', - lte: '≤', - contains: '∋', - in: '∈', -} as const +import { filterRulesToFilter, filterToRules } from '@/lib/table/query-builder/converters' + +const OPERATOR_LABELS = Object.fromEntries( + COMPARISON_OPERATORS.map((op) => [op.value, op.label]) +) as Record interface TableFilterProps { columns: Array<{ name: string; type: string }> + filter: Filter | null onApply: (filter: Filter | null) => void + onClose: () => void } -export function TableFilter({ columns, onApply }: TableFilterProps) { - const [rules, setRules] = useState(() => [createRule(columns)]) +export function TableFilter({ columns, filter, onApply, onClose }: TableFilterProps) { + const [rules, setRules] = useState(() => { + const fromFilter = filterToRules(filter) + return fromFilter.length > 0 ? fromFilter : [createRule(columns)] + }) const columnOptions = useMemo( () => columns.map((col) => ({ value: col.name, label: col.name })), @@ -46,52 +43,82 @@ export function TableFilter({ columns, onApply }: TableFilterProps) { const handleRemove = useCallback( (id: string) => { - setRules((prev) => { - const next = prev.filter((r) => r.id !== id) - return next.length === 0 ? [createRule(columns)] : next - }) + const next = rules.filter((r) => r.id !== id) + if (next.length === 0) { + onApply(null) + onClose() + setRules([createRule(columns)]) + } else { + setRules(next) + } }, - [columns] + [columns, onApply, onClose, rules] ) const handleUpdate = useCallback((id: string, field: keyof FilterRule, value: string) => { setRules((prev) => prev.map((r) => (r.id === id ? { ...r, [field]: value } : r))) }, []) + const handleToggleLogical = useCallback((id: string) => { + setRules((prev) => + prev.map((r) => + r.id === id ? { ...r, logicalOperator: r.logicalOperator === 'and' ? 'or' : 'and' } : r + ) + ) + }, []) + const handleApply = useCallback(() => { const validRules = rules.filter((r) => r.column && r.value) onApply(filterRulesToFilter(validRules)) }, [rules, onApply]) - return ( -
- {rules.map((rule) => ( - - ))} - -
- + const handleClear = useCallback(() => { + setRules([createRule(columns)]) + onApply(null) + }, [columns, onApply]) - + return ( +
+
+ {rules.map((rule, index) => ( + + ))} + +
+ +
+ {filter !== null && ( + + )} + +
+
) @@ -99,18 +126,39 @@ export function TableFilter({ columns, onApply }: TableFilterProps) { interface FilterRuleRowProps { rule: FilterRule + isFirst: boolean columns: Array<{ value: string; label: string }> onUpdate: (id: string, field: keyof FilterRule, value: string) => void onRemove: (id: string) => void onApply: () => void + onToggleLogical: (id: string) => void } -function FilterRuleRow({ rule, columns, onUpdate, onRemove, onApply }: FilterRuleRowProps) { +const FilterRuleRow = memo(function FilterRuleRow({ + rule, + isFirst, + columns, + onUpdate, + onRemove, + onApply, + onToggleLogical, +}: FilterRuleRowProps) { return ( -
+
+ {isFirst ? ( + Where + ) : ( + + )} + - @@ -129,8 +177,8 @@ function FilterRuleRow({ rule, columns, onUpdate, onRemove, onApply }: FilterRul - @@ -151,25 +199,21 @@ function FilterRuleRow({ rule, columns, onUpdate, onRemove, onApply }: FilterRul value={rule.value} onChange={(e) => onUpdate(rule.id, 'value', e.target.value)} onKeyDown={(e) => { - if (e.key === 'Enter') handleApply() + if (e.key === 'Enter') onApply() }} placeholder='Enter a value' - className='h-[30px] min-w-[160px] flex-1 rounded-[5px] border border-[var(--border)] bg-transparent px-2 text-[var(--text-secondary)] text-xs outline-none placeholder:text-[var(--text-subtle)] hover-hover:border-[var(--border-1)] focus:border-[var(--border-1)]' + className='h-[28px] flex-1 rounded-[5px] border border-[var(--border)] bg-transparent px-2 text-[var(--text-secondary)] text-xs outline-none placeholder:text-[var(--text-subtle)] hover-hover:border-[var(--border-1)] focus:border-[var(--border-1)]' />
) - - function handleApply() { - onApply() - } -} +}) function createRule(columns: Array<{ name: string }>): FilterRule { return { diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx index 74f116454d2..587818ac76f 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx @@ -1,6 +1,7 @@ 'use client' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { GripVertical } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' import { Button, @@ -305,6 +306,17 @@ export function Table({ return 0 }, [resizingColumn, displayColumns, columnWidths]) + const dropIndicatorLeft = useMemo(() => { + if (!dropTargetColumnName) return null + let left = CHECKBOX_COL_WIDTH + for (const col of displayColumns) { + if (dropSide === 'left' && col.name === dropTargetColumnName) return left + left += columnWidths[col.name] ?? COL_WIDTH + if (dropSide === 'right' && col.name === dropTargetColumnName) return left + } + return null + }, [dropTargetColumnName, dropSide, displayColumns, columnWidths]) + const isAllRowsSelected = useMemo(() => { if (checkedRows.size > 0 && rows.length > 0 && checkedRows.size >= rows.length) { for (const row of rows) { @@ -1448,6 +1460,13 @@ export function Table({ const handleFilterApply = useCallback((filter: Filter | null) => { setQueryOptions((prev) => ({ ...prev, filter })) }, []) + + const [filterOpen, setFilterOpen] = useState(false) + + const handleFilterToggle = useCallback(() => { + setFilterOpen((prev) => !prev) + }, []) + const columnOptions = useMemo( () => displayColumns.map((col) => ({ @@ -1526,11 +1545,6 @@ export function Table({ [handleAddColumn, addColumnMutation.isPending] ) - const filterElement = useMemo( - () => , - [displayColumns, handleFilterApply] - ) - const activeSortState = useMemo(() => { if (!queryOptions.sort) return null const entries = Object.entries(queryOptions.sort) @@ -1597,7 +1611,19 @@ export function Table({ <> - + + {filterOpen && ( + + )} )} @@ -1677,8 +1703,6 @@ export function Table({ onResize={handleColumnResize} onResizeEnd={handleColumnResizeEnd} isDragging={dragColumnName === column.name} - isDropTarget={dropTargetColumnName === column.name} - dropSide={dropTargetColumnName === column.name ? dropSide : undefined} onDragStart={handleColumnDragStart} onDragOver={handleColumnDragOver} onDragEnd={handleColumnDragEnd} @@ -1701,7 +1725,7 @@ export function Table({ <> {rows.map((row, index) => { const prevPosition = index > 0 ? rows[index - 1].position : -1 - const gapCount = row.position - prevPosition - 1 + const gapCount = queryOptions.filter ? 0 : row.position - prevPosition - 1 return ( {gapCount > 0 && ( @@ -1755,6 +1779,12 @@ export function Table({ style={{ left: resizeIndicatorLeft }} /> )} + {dropIndicatorLeft !== null && ( +
+ )}
{!isLoadingTable && !isLoadingRows && userPermissions.canEdit && ( @@ -2581,8 +2611,6 @@ const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({ onResize, onResizeEnd, isDragging, - isDropTarget, - dropSide, onDragStart, onDragOver, onDragEnd, @@ -2605,8 +2633,6 @@ const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({ onResize: (columnName: string, width: number) => void onResizeEnd: () => void isDragging?: boolean - isDropTarget?: boolean - dropSide?: 'left' | 'right' onDragStart?: (columnName: string) => void onDragOver?: (columnName: string, side: 'left' | 'right') => void onDragEnd?: () => void @@ -2698,22 +2724,13 @@ const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({ return ( - {isDropTarget && dropSide === 'left' && ( -
- )} - {isDropTarget && dropSide === 'right' && ( -
- )} {isRenaming ? (
@@ -2738,63 +2755,73 @@ const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
) : ( - - - - - - onRenameColumn(column.name)}> - - Rename column - - - - {React.createElement(COLUMN_TYPE_ICONS[column.type] ?? TypeText)} - Change type - - - {COLUMN_TYPE_OPTIONS.map((option) => ( - onChangeType(column.name, option.type)} - > - - {option.label} - - ))} - - - - onInsertLeft(column.name)}> - - Insert column left - - onInsertRight(column.name)}> - - Insert column right - - - onToggleUnique(column.name)}> - - {column.unique ? 'Remove unique' : 'Set unique'} - - - onDeleteColumn(column.name)}> - - Delete column - - - +
+ + + + + + onRenameColumn(column.name)}> + + Rename column + + + + {React.createElement(COLUMN_TYPE_ICONS[column.type] ?? TypeText)} + Change type + + + {COLUMN_TYPE_OPTIONS.map((option) => ( + onChangeType(column.name, option.type)} + > + + {option.label} + + ))} + + + + onInsertLeft(column.name)}> + + Insert column left + + onInsertRight(column.name)}> + + Insert column right + + + onToggleUnique(column.name)}> + + {column.unique ? 'Remove unique' : 'Set unique'} + + + onDeleteColumn(column.name)}> + + Delete column + + + +
+ +
+
)}
Date: Sat, 28 Mar 2026 18:08:28 -0700 Subject: [PATCH 02/30] fix(table-filter): use ref to stabilize handleRemove/handleApply callbacks Reading rules via ref instead of closure eliminates rules from useCallback dependency arrays, keeping callbacks stable across rule edits and preserving the memo() benefit on FilterRuleRow. --- .../components/table-filter/table-filter.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx index 43e3d16d657..5df1af01171 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx @@ -1,6 +1,6 @@ 'use client' -import { memo, useCallback, useMemo, useState } from 'react' +import { memo, useCallback, useMemo, useRef, useState } from 'react' import { X } from 'lucide-react' import { nanoid } from 'nanoid' import { @@ -32,6 +32,11 @@ export function TableFilter({ columns, filter, onApply, onClose }: TableFilterPr return fromFilter.length > 0 ? fromFilter : [createRule(columns)] }) + // Ref kept in sync each render so callbacks can read current rules + // without capturing them in their dependency arrays (keeps memo stable) + const rulesRef = useRef(rules) + rulesRef.current = rules + const columnOptions = useMemo( () => columns.map((col) => ({ value: col.name, label: col.name })), [columns] @@ -43,7 +48,7 @@ export function TableFilter({ columns, filter, onApply, onClose }: TableFilterPr const handleRemove = useCallback( (id: string) => { - const next = rules.filter((r) => r.id !== id) + const next = rulesRef.current.filter((r) => r.id !== id) if (next.length === 0) { onApply(null) onClose() @@ -52,7 +57,7 @@ export function TableFilter({ columns, filter, onApply, onClose }: TableFilterPr setRules(next) } }, - [columns, onApply, onClose, rules] + [columns, onApply, onClose] ) const handleUpdate = useCallback((id: string, field: keyof FilterRule, value: string) => { @@ -68,9 +73,9 @@ export function TableFilter({ columns, filter, onApply, onClose }: TableFilterPr }, []) const handleApply = useCallback(() => { - const validRules = rules.filter((r) => r.column && r.value) + const validRules = rulesRef.current.filter((r) => r.column && r.value) onApply(filterRulesToFilter(validRules)) - }, [rules, onApply]) + }, [onApply]) const handleClear = useCallback(() => { setRules([createRule(columns)]) From 5d037acdc8ab92f8d84876e39de12ab487e4a53b Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 18:14:08 -0700 Subject: [PATCH 03/30] improvement(tables,kb): remove hacky patterns, fix KB filter popover width - Remove non-TSDoc comment from table-filter (rulesRef pattern is self-evident) - Simplify SearchSection: remove setState-during-render anti-pattern; controlled input binds directly to search.value/onChange (simpler and correct) - Reduce KB filter popover from w-[320px] to w-[200px]; tag filter uses vertical layout so narrow width works; Status-only case is now appropriately compact --- .../resource-options-bar.tsx | 38 +++---------------- .../[workspaceId]/knowledge/[id]/base.tsx | 2 +- .../components/table-filter/table-filter.tsx | 2 - 3 files changed, 6 insertions(+), 36 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx index 3d788e1aac5..12a4434a1bb 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx @@ -1,4 +1,4 @@ -import { memo, type ReactNode, useCallback, useRef, useState } from 'react' +import { memo, type ReactNode } from 'react' import * as PopoverPrimitive from '@radix-ui/react-popover' import { ArrowDown, @@ -149,34 +149,6 @@ export const ResourceOptionsBar = memo(function ResourceOptionsBar({ }) 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) => { - 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 (
{SEARCH_ICON} @@ -198,8 +170,8 @@ const SearchSection = memo(function SearchSection({ search }: { search: SearchCo search.onChange(e.target.value)} onKeyDown={search.onKeyDown} onFocus={search.onFocus} onBlur={search.onBlur} @@ -207,11 +179,11 @@ const SearchSection = memo(function SearchSection({ search }: { search: SearchCo className='min-w-[80px] flex-1 bg-transparent py-1 text-[var(--text-secondary)] text-caption outline-none placeholder:text-[var(--text-subtle)]' />
- {search.tags?.length || localValue ? ( + {search.tags?.length || search.value ? ( diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index 5cd3c25a243..355d1e026d0 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -813,7 +813,7 @@ export function KnowledgeBase({ } const filterContent = ( -
+
Status
diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx index 5df1af01171..07d5046283b 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx @@ -32,8 +32,6 @@ export function TableFilter({ columns, filter, onApply, onClose }: TableFilterPr return fromFilter.length > 0 ? fromFilter : [createRule(columns)] }) - // Ref kept in sync each render so callbacks can read current rules - // without capturing them in their dependency arrays (keeps memo stable) const rulesRef = useRef(rules) rulesRef.current = rules From 2e678642bf001c103977274568101515abeb28be Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 18:19:21 -0700 Subject: [PATCH 04/30] feat(knowledge): add sort and filter to KB list page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sort dropdown: name, documents, tokens, created, last updated — pre-sorted externally before passing rows to Resource. Active sort highlights the Sort button; clear resets to default (created desc). Filter popover: filter by connector status (All / With connectors / Without connectors). Active filter shown as a removable tag in the toolbar. --- .../[workspaceId]/knowledge/knowledge.tsx | 123 ++++++++++++++++-- 1 file changed, 109 insertions(+), 14 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx index 46ee5efdbe4..145cf1e19db 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx @@ -5,13 +5,16 @@ import { createLogger } from '@sim/logger' import { useParams, useRouter } from 'next/navigation' import { Tooltip } from '@/components/emcn' import { Database } from '@/components/emcn/icons' +import { cn } from '@/lib/core/utils/cn' import type { KnowledgeBaseData } from '@/lib/knowledge/types' import type { CreateAction, + FilterTag, ResourceCell, ResourceColumn, ResourceRow, SearchConfig, + SortConfig, } from '@/app/workspace/[workspaceId]/components' import { ownerCell, Resource, timeCell } from '@/app/workspace/[workspaceId]/components' import { BaseTagsModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/components' @@ -98,6 +101,12 @@ export function Knowledge() { const { mutateAsync: updateKnowledgeBaseMutation } = useUpdateKnowledgeBase(workspaceId) const { mutateAsync: deleteKnowledgeBaseMutation } = useDeleteKnowledgeBase(workspaceId) + const [activeSort, setActiveSort] = useState<{ + column: string + direction: 'asc' | 'desc' + } | null>(null) + const [connectorFilter, setConnectorFilter] = useState<'all' | 'connected' | 'unconnected'>('all') + const [searchInputValue, setSearchInputValue] = useState('') const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('') const searchTimerRef = useRef>(null) @@ -184,14 +193,47 @@ export function Knowledge() { [deleteKnowledgeBaseMutation] ) - const filteredKnowledgeBases = useMemo( - () => filterKnowledgeBases(knowledgeBases, debouncedSearchQuery), - [knowledgeBases, debouncedSearchQuery] - ) + const processedKBs = useMemo(() => { + let result = filterKnowledgeBases(knowledgeBases, debouncedSearchQuery) + + if (connectorFilter !== 'all') { + result = result.filter((kb) => + connectorFilter === 'connected' + ? (kb.connectorTypes?.length ?? 0) > 0 + : (kb.connectorTypes?.length ?? 0) === 0 + ) + } + + const col = activeSort?.column ?? 'created' + const dir = activeSort?.direction ?? 'desc' + return [...result].sort((a, b) => { + let cmp = 0 + switch (col) { + case 'name': + cmp = a.name.localeCompare(b.name) + break + case 'documents': + cmp = + ((a as KnowledgeBaseWithDocCount).docCount || 0) - + ((b as KnowledgeBaseWithDocCount).docCount || 0) + break + case 'tokens': + cmp = (a.tokenCount || 0) - (b.tokenCount || 0) + break + case 'created': + cmp = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + break + case 'updated': + cmp = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime() + break + } + return dir === 'asc' ? cmp : -cmp + }) + }, [knowledgeBases, debouncedSearchQuery, connectorFilter, activeSort]) const rows: ResourceRow[] = useMemo( () => - filteredKnowledgeBases.map((kb) => { + processedKBs.map((kb) => { const kbWithCount = kb as KnowledgeBaseWithDocCount return { id: kb.id, @@ -211,16 +253,9 @@ export function Knowledge() { owner: ownerCell(kb.userId, members), updated: timeCell(kb.updatedAt), }, - sortValues: { - documents: kbWithCount.docCount || 0, - tokens: kb.tokenCount || 0, - connectors: kb.connectorTypes?.length || 0, - created: -new Date(kb.createdAt).getTime(), - updated: -new Date(kb.updatedAt).getTime(), - }, } }), - [filteredKnowledgeBases, members] + [processedKBs, members] ) const handleRowClick = useCallback( @@ -310,6 +345,64 @@ export function Knowledge() { [searchInputValue, handleSearchChange, handleSearchClearAll] ) + const sortConfig: SortConfig = useMemo( + () => ({ + options: [ + { id: 'name', label: 'Name' }, + { id: 'documents', label: 'Documents' }, + { id: 'tokens', label: 'Tokens' }, + { id: 'created', label: 'Created' }, + { id: 'updated', label: 'Last Updated' }, + ], + active: activeSort, + onSort: (column, direction) => setActiveSort({ column, direction }), + onClear: () => setActiveSort(null), + }), + [activeSort] + ) + + const filterContent = ( +
+
+ Connectors +
+
+ {( + [ + { value: 'all', label: 'All' }, + { value: 'connected', label: 'With connectors' }, + { value: 'unconnected', label: 'Without connectors' }, + ] as const + ).map(({ value, label }) => ( + + ))} +
+
+ ) + + const filterTags: FilterTag[] = useMemo( + () => + connectorFilter === 'all' + ? [] + : [ + { + label: connectorFilter === 'connected' ? 'Connectors: Active' : 'Connectors: None', + onRemove: () => setConnectorFilter('all'), + }, + ], + [connectorFilter] + ) + return ( <> Date: Sat, 28 Mar 2026 18:22:52 -0700 Subject: [PATCH 05/30] feat(files): add sort and filter to files list page --- .../workspace/[workspaceId]/files/files.tsx | 130 ++++++++++++++++-- .../scheduled-tasks/scheduled-tasks.tsx | 5 + 2 files changed, 124 insertions(+), 11 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 221b658c4df..9cd902484a6 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -25,6 +25,7 @@ import { } from '@/components/emcn' import { File as FilesIcon } from '@/components/emcn/icons' import { getDocumentIcon } from '@/components/icons/document-icons' +import { cn } from '@/lib/core/utils/cn' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' import { downloadWorkspaceFile, @@ -38,10 +39,12 @@ import { SUPPORTED_VIDEO_EXTENSIONS, } from '@/lib/uploads/utils/validation' import type { + FilterTag, HeaderAction, ResourceColumn, ResourceRow, SearchConfig, + SortConfig, } from '@/app/workspace/[workspaceId]/components' import { InlineRenameInput, @@ -162,6 +165,11 @@ export function Files() { const [uploadProgress, setUploadProgress] = useState({ completed: 0, total: 0 }) const [inputValue, setInputValue] = useState('') const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('') + const [activeSort, setActiveSort] = useState<{ + column: string + direction: 'asc' | 'desc' + } | null>(null) + const [typeFilter, setTypeFilter] = useState<'all' | 'document' | 'audio' | 'video'>('all') const searchTimerRef = useRef>(null) const handleSearchChange = useCallback((value: string) => { @@ -206,10 +214,51 @@ export function Files() { selectedFileRef.current = selectedFile const filteredFiles = useMemo(() => { - if (!debouncedSearchTerm) return files - const q = debouncedSearchTerm.toLowerCase() - return files.filter((f) => f.name.toLowerCase().includes(q)) - }, [files, debouncedSearchTerm]) + let result = debouncedSearchTerm + ? files.filter((f) => f.name.toLowerCase().includes(debouncedSearchTerm.toLowerCase())) + : files + + if (typeFilter !== 'all') { + result = result.filter((f) => { + const ext = getFileExtension(f.name) + if (typeFilter === 'document') + return SUPPORTED_DOCUMENT_EXTENSIONS.includes( + ext as (typeof SUPPORTED_DOCUMENT_EXTENSIONS)[number] + ) + if (typeFilter === 'audio') + return SUPPORTED_AUDIO_EXTENSIONS.includes( + ext as (typeof SUPPORTED_AUDIO_EXTENSIONS)[number] + ) + if (typeFilter === 'video') + return SUPPORTED_VIDEO_EXTENSIONS.includes( + ext as (typeof SUPPORTED_VIDEO_EXTENSIONS)[number] + ) + return true + }) + } + + const col = activeSort?.column ?? 'created' + const dir = activeSort?.direction ?? 'desc' + return [...result].sort((a, b) => { + let cmp = 0 + switch (col) { + case 'name': + cmp = a.name.localeCompare(b.name) + break + case 'size': + cmp = a.size - b.size + break + case 'type': + cmp = formatFileType(a.type, a.name).localeCompare(formatFileType(b.type, b.name)) + break + case 'created': + case 'updated': + cmp = new Date(a.uploadedAt).getTime() - new Date(b.uploadedAt).getTime() + break + } + return dir === 'asc' ? cmp : -cmp + }) + }, [files, debouncedSearchTerm, typeFilter, activeSort]) const rowCacheRef = useRef( new Map() @@ -247,11 +296,6 @@ export function Files() { owner: ownerCell(file.uploadedBy, members), updated: timeCell(file.uploadedAt), }, - sortValues: { - size: file.size, - created: -new Date(file.uploadedAt).getTime(), - updated: -new Date(file.uploadedAt).getTime(), - }, } nextCache.set(file.id, { row, file, members }) return row @@ -690,7 +734,6 @@ export function Files() { handleDeleteSelected, ]) - /** Stable refs for values used in callbacks to avoid dependency churn */ const listRenameRef = useRef(listRename) listRenameRef.current = listRename const headerRenameRef = useRef(headerRename) @@ -764,6 +807,69 @@ export function Files() { [handleNavigateToFiles] ) + const sortConfig: SortConfig = useMemo( + () => ({ + options: [ + { id: 'name', label: 'Name' }, + { id: 'size', label: 'Size' }, + { id: 'type', label: 'Type' }, + { id: 'created', label: 'Created' }, + ], + active: activeSort, + onSort: (column, direction) => setActiveSort({ column, direction }), + onClear: () => setActiveSort(null), + }), + [activeSort] + ) + + const filterContent = ( +
+
+ File Type +
+
+ {( + [ + { value: 'all', label: 'All' }, + { value: 'document', label: 'Documents' }, + { value: 'audio', label: 'Audio' }, + { value: 'video', label: 'Video' }, + ] as const + ).map(({ value, label }) => ( + + ))} +
+
+ ) + + const filterTags: FilterTag[] = useMemo( + () => + typeFilter === 'all' + ? [] + : [ + { + label: + typeFilter === 'document' + ? 'Type: Documents' + : typeFilter === 'audio' + ? 'Type: Audio' + : 'Type: Video', + onRemove: () => setTypeFilter('all'), + }, + ], + [typeFilter] + ) + if (fileIdFromRoute && !selectedFile) { return (
@@ -834,7 +940,9 @@ export function Files() { title='Files' create={createConfig} search={searchConfig} - defaultSort='created' + sort={sortConfig} + filter={filterContent} + filterTags={filterTags} headerActions={headerActionsConfig} columns={COLUMNS} rows={rows} diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx index 36acbbe08ed..f5748f45dc6 100644 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx +++ b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx @@ -74,6 +74,11 @@ export function ScheduledTasks() { const [activeTask, setActiveTask] = useState(null) const [searchQuery, setSearchQuery] = useState('') const debouncedSearchQuery = useDebounce(searchQuery, 300) + const [activeSort, setActiveSort] = useState<{ + column: string + direction: 'asc' | 'desc' + } | null>(null) + const [scheduleTypeFilter, setScheduleTypeFilter] = useState<'all' | 'recurring' | 'once'>('all') const visibleItems = useMemo( () => allItems.filter((item) => item.sourceType === 'job' && item.status !== 'completed'), From 6c18471e094be7fb20354573b6aa15e7f797d487 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 18:23:47 -0700 Subject: [PATCH 06/30] feat(scheduled-tasks): add sort and filter to scheduled tasks page --- .../scheduled-tasks/scheduled-tasks.tsx | 115 ++++++++++++++++-- 1 file changed, 102 insertions(+), 13 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx index f5748f45dc6..172102c3d42 100644 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx +++ b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx @@ -5,9 +5,15 @@ import { createLogger } from '@sim/logger' import { useParams } from 'next/navigation' import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn' import { Calendar } from '@/components/emcn/icons' +import { cn } from '@/lib/core/utils/cn' import { formatAbsoluteDate } from '@/lib/core/utils/formatting' import { parseCronToHumanReadable } from '@/lib/workflows/schedules/utils' -import type { ResourceColumn, ResourceRow } from '@/app/workspace/[workspaceId]/components' +import type { + FilterTag, + ResourceColumn, + ResourceRow, + SortConfig, +} from '@/app/workspace/[workspaceId]/components' import { Resource, timeCell } from '@/app/workspace/[workspaceId]/components' import { ScheduleModal } from '@/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal' import { ScheduleContextMenu } from '@/app/workspace/[workspaceId]/scheduled-tasks/components/schedule-context-menu' @@ -86,15 +92,44 @@ export function ScheduledTasks() { ) const filteredItems = useMemo(() => { - if (!debouncedSearchQuery) return visibleItems - const q = debouncedSearchQuery.toLowerCase() - return visibleItems.filter((item) => { - const task = item.prompt || '' - return ( - task.toLowerCase().includes(q) || getScheduleDescription(item).toLowerCase().includes(q) + let result = debouncedSearchQuery + ? visibleItems.filter((item) => { + const task = item.prompt || '' + return ( + task.toLowerCase().includes(debouncedSearchQuery.toLowerCase()) || + getScheduleDescription(item).toLowerCase().includes(debouncedSearchQuery.toLowerCase()) + ) + }) + : visibleItems + + if (scheduleTypeFilter !== 'all') { + result = result.filter((item) => + scheduleTypeFilter === 'recurring' ? Boolean(item.cronExpression) : !item.cronExpression ) + } + + const col = activeSort?.column ?? 'nextRun' + const dir = activeSort?.direction ?? 'desc' + return [...result].sort((a, b) => { + let cmp = 0 + switch (col) { + case 'task': + cmp = (a.prompt || '').localeCompare(b.prompt || '') + break + case 'nextRun': + cmp = + (a.nextRunAt ? new Date(a.nextRunAt).getTime() : 0) - + (b.nextRunAt ? new Date(b.nextRunAt).getTime() : 0) + break + case 'lastRun': + cmp = + (a.lastRanAt ? new Date(a.lastRanAt).getTime() : 0) - + (b.lastRanAt ? new Date(b.lastRanAt).getTime() : 0) + break + } + return dir === 'asc' ? cmp : -cmp }) - }, [visibleItems, debouncedSearchQuery]) + }, [visibleItems, debouncedSearchQuery, scheduleTypeFilter, activeSort]) const rows: ResourceRow[] = useMemo( () => @@ -109,10 +144,6 @@ export function ScheduledTasks() { nextRun: timeCell(item.nextRunAt), lastRun: timeCell(item.lastRanAt), }, - sortValues: { - nextRun: item.nextRunAt ? -new Date(item.nextRunAt).getTime() : 0, - lastRun: item.lastRanAt ? -new Date(item.lastRanAt).getTime() : 0, - }, })), [filteredItems] ) @@ -175,6 +206,62 @@ export function ScheduledTasks() { } } + const sortConfig: SortConfig = useMemo( + () => ({ + options: [ + { id: 'task', label: 'Task' }, + { id: 'nextRun', label: 'Next Run' }, + { id: 'lastRun', label: 'Last Run' }, + ], + active: activeSort, + onSort: (column, direction) => setActiveSort({ column, direction }), + onClear: () => setActiveSort(null), + }), + [activeSort] + ) + + const filterContent = ( +
+
+ Schedule Type +
+
+ {( + [ + { value: 'all', label: 'All' }, + { value: 'recurring', label: 'Recurring' }, + { value: 'once', label: 'One-time' }, + ] as const + ).map(({ value, label }) => ( + + ))} +
+
+ ) + + const filterTags: FilterTag[] = useMemo( + () => + scheduleTypeFilter === 'all' + ? [] + : [ + { + label: scheduleTypeFilter === 'recurring' ? 'Type: Recurring' : 'Type: One-time', + onRemove: () => setScheduleTypeFilter('all'), + }, + ], + [scheduleTypeFilter] + ) + return ( <> Date: Sat, 28 Mar 2026 18:28:20 -0700 Subject: [PATCH 07/30] fix(table-filter): use explicit close handler instead of toggle --- .../tables/[tableId]/components/table/table.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx index 587818ac76f..d0189f4b92f 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx @@ -1467,6 +1467,10 @@ export function Table({ setFilterOpen((prev) => !prev) }, []) + const handleFilterClose = useCallback(() => { + setFilterOpen(false) + }, []) + const columnOptions = useMemo( () => displayColumns.map((col) => ({ @@ -1621,7 +1625,7 @@ export function Table({ columns={displayColumns} filter={queryOptions.filter} onApply={handleFilterApply} - onClose={handleFilterToggle} + onClose={handleFilterClose} /> )} From 8a1f3dc64bb47589a17c507e9282c1780f1f5141 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 18:33:04 -0700 Subject: [PATCH 08/30] improvement(files,knowledge): replace manual debounce with useDebounce hook and use type guards for file filtering --- .../workspace/[workspaceId]/files/files.tsx | 40 +++++-------------- .../[workspaceId]/knowledge/knowledge.tsx | 22 +++------- 2 files changed, 16 insertions(+), 46 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 9cd902484a6..f458ffc2af6 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -34,6 +34,9 @@ import { getMimeTypeFromExtension, } from '@/lib/uploads/utils/file-utils' import { + isSupportedAudioExtension, + isSupportedExtension, + isSupportedVideoExtension, SUPPORTED_AUDIO_EXTENSIONS, SUPPORTED_DOCUMENT_EXTENSIONS, SUPPORTED_VIDEO_EXTENSIONS, @@ -69,6 +72,7 @@ import { useUploadWorkspaceFile, useWorkspaceFiles, } from '@/hooks/queries/workspace-files' +import { useDebounce } from '@/hooks/use-debounce' import { useInlineRename } from '@/hooks/use-inline-rename' type SaveStatus = 'idle' | 'saving' | 'saved' | 'error' @@ -164,21 +168,12 @@ export function Files() { const [uploading, setUploading] = useState(false) const [uploadProgress, setUploadProgress] = useState({ completed: 0, total: 0 }) const [inputValue, setInputValue] = useState('') - const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('') + const debouncedSearchTerm = useDebounce(inputValue, 200) const [activeSort, setActiveSort] = useState<{ column: string direction: 'asc' | 'desc' } | null>(null) const [typeFilter, setTypeFilter] = useState<'all' | 'document' | 'audio' | 'video'>('all') - const searchTimerRef = useRef>(null) - - const handleSearchChange = useCallback((value: string) => { - setInputValue(value) - if (searchTimerRef.current) clearTimeout(searchTimerRef.current) - searchTimerRef.current = setTimeout(() => { - setDebouncedSearchTerm(value) - }, 200) - }, []) const [creatingFile, setCreatingFile] = useState(false) const [isDirty, setIsDirty] = useState(false) @@ -221,18 +216,9 @@ export function Files() { if (typeFilter !== 'all') { result = result.filter((f) => { const ext = getFileExtension(f.name) - if (typeFilter === 'document') - return SUPPORTED_DOCUMENT_EXTENSIONS.includes( - ext as (typeof SUPPORTED_DOCUMENT_EXTENSIONS)[number] - ) - if (typeFilter === 'audio') - return SUPPORTED_AUDIO_EXTENSIONS.includes( - ext as (typeof SUPPORTED_AUDIO_EXTENSIONS)[number] - ) - if (typeFilter === 'video') - return SUPPORTED_VIDEO_EXTENSIONS.includes( - ext as (typeof SUPPORTED_VIDEO_EXTENSIONS)[number] - ) + if (typeFilter === 'document') return isSupportedExtension(ext) + if (typeFilter === 'audio') return isSupportedAudioExtension(ext) + if (typeFilter === 'video') return isSupportedVideoExtension(ext) return true }) } @@ -754,18 +740,14 @@ export function Files() { const canEdit = userPermissions.canEdit === true - const handleSearchClearAll = useCallback(() => { - handleSearchChange('') - }, [handleSearchChange]) - const searchConfig: SearchConfig = useMemo( () => ({ value: inputValue, - onChange: handleSearchChange, - onClearAll: handleSearchClearAll, + onChange: setInputValue, + onClearAll: () => setInputValue(''), placeholder: 'Search files...', }), - [inputValue, handleSearchChange, handleSearchClearAll] + [inputValue] ) const createConfig = useMemo( diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx index 145cf1e19db..ba1d724fdb4 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx @@ -32,6 +32,7 @@ import { CONNECTOR_REGISTRY } from '@/connectors/registry' 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') @@ -108,20 +109,7 @@ export function Knowledge() { const [connectorFilter, setConnectorFilter] = useState<'all' | 'connected' | 'unconnected'>('all') const [searchInputValue, setSearchInputValue] = useState('') - const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('') - const searchTimerRef = useRef>(null) - - const handleSearchChange = useCallback((value: string) => { - setSearchInputValue(value) - if (searchTimerRef.current) clearTimeout(searchTimerRef.current) - searchTimerRef.current = setTimeout(() => { - setDebouncedSearchQuery(value) - }, 300) - }, []) - - const handleSearchClearAll = useCallback(() => { - handleSearchChange('') - }, [handleSearchChange]) + const debouncedSearchQuery = useDebounce(searchInputValue, 300) const [isCreateModalOpen, setIsCreateModalOpen] = useState(false) @@ -338,11 +326,11 @@ export function Knowledge() { const searchConfig: SearchConfig = useMemo( () => ({ value: searchInputValue, - onChange: handleSearchChange, - onClearAll: handleSearchClearAll, + onChange: setSearchInputValue, + onClearAll: () => setSearchInputValue(''), placeholder: 'Search knowledge bases...', }), - [searchInputValue, handleSearchChange, handleSearchClearAll] + [searchInputValue] ) const sortConfig: SortConfig = useMemo( From 4899dc32e8e8c94a0855fceab7d9fc56fd4c060a Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 18:35:24 -0700 Subject: [PATCH 09/30] fix(resource): prevent popover from inheriting anchor min-width --- .../components/resource-options-bar/resource-options-bar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx index 12a4434a1bb..d8e09d813c7 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx @@ -134,7 +134,7 @@ export const ResourceOptionsBar = memo(function ResourceOptionsBar({ {filter} From f6edb88d814c292792388a247a3a95eecb4ef706 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 18:35:30 -0700 Subject: [PATCH 10/30] feat(tables): add sort to tables list page --- .../workspace/[workspaceId]/tables/tables.tsx | 91 +++++++++++++++---- 1 file changed, 71 insertions(+), 20 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx b/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx index 8b8a1178122..33ca3ca85bd 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx @@ -16,7 +16,12 @@ import { import { Columns3, Rows3, Table as TableIcon } from '@/components/emcn/icons' import type { TableDefinition } from '@/lib/table' import { generateUniqueTableName } from '@/lib/table/constants' -import type { ResourceColumn, ResourceRow } from '@/app/workspace/[workspaceId]/components' +import type { + ResourceColumn, + ResourceRow, + SearchConfig, + SortConfig, +} from '@/app/workspace/[workspaceId]/components' import { ownerCell, Resource, timeCell } from '@/app/workspace/[workspaceId]/components' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { TablesListContextMenu } from '@/app/workspace/[workspaceId]/tables/components' @@ -29,6 +34,7 @@ import { useUploadCsvToTable, } from '@/hooks/queries/tables' import { useWorkspaceMembersQuery } from '@/hooks/queries/workspace' +import { useDebounce } from '@/hooks/use-debounce' const logger = createLogger('Tables') @@ -60,6 +66,11 @@ export function Tables() { const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) const [activeTable, setActiveTable] = useState(null) const [searchTerm, setSearchTerm] = useState('') + const debouncedSearchTerm = useDebounce(searchTerm, 300) + const [activeSort, setActiveSort] = useState<{ + column: string + direction: 'asc' | 'desc' + } | null>(null) const [uploading, setUploading] = useState(false) const [uploadProgress, setUploadProgress] = useState({ completed: 0, total: 0 }) const csvInputRef = useRef(null) @@ -78,15 +89,39 @@ export function Tables() { closeMenu: closeRowContextMenu, } = useContextMenu() - const filteredTables = useMemo(() => { - if (!searchTerm) return tables - const term = searchTerm.toLowerCase() - return tables.filter((table) => table.name.toLowerCase().includes(term)) - }, [tables, searchTerm]) + const processedTables = useMemo(() => { + const result = debouncedSearchTerm + ? tables.filter((t) => t.name.toLowerCase().includes(debouncedSearchTerm.toLowerCase())) + : tables + + const col = activeSort?.column ?? 'created' + const dir = activeSort?.direction ?? 'desc' + return [...result].sort((a, b) => { + let cmp = 0 + switch (col) { + case 'name': + cmp = a.name.localeCompare(b.name) + break + case 'columns': + cmp = a.schema.columns.length - b.schema.columns.length + break + case 'rows': + cmp = a.rowCount - b.rowCount + break + case 'created': + cmp = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + break + case 'updated': + cmp = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime() + break + } + return dir === 'asc' ? cmp : -cmp + }) + }, [tables, debouncedSearchTerm, activeSort]) const rows: ResourceRow[] = useMemo( () => - filteredTables.map((table) => ({ + processedTables.map((table) => ({ id: table.id, cells: { name: { @@ -105,14 +140,34 @@ export function Tables() { owner: ownerCell(table.createdBy, members), updated: timeCell(table.updatedAt), }, - sortValues: { - columns: table.schema.columns.length, - rows: table.rowCount, - created: -new Date(table.createdAt).getTime(), - updated: -new Date(table.updatedAt).getTime(), - }, })), - [filteredTables, members] + [processedTables, members] + ) + + const searchConfig: SearchConfig = useMemo( + () => ({ + value: searchTerm, + onChange: setSearchTerm, + onClearAll: () => setSearchTerm(''), + placeholder: 'Search tables...', + }), + [searchTerm] + ) + + const sortConfig: SortConfig = useMemo( + () => ({ + options: [ + { id: 'name', label: 'Name' }, + { id: 'columns', label: 'Columns' }, + { id: 'rows', label: 'Rows' }, + { id: 'created', label: 'Created' }, + { id: 'updated', label: 'Last Updated' }, + ], + active: activeSort, + onSort: (column, direction) => setActiveSort({ column, direction }), + onClear: () => setActiveSort(null), + }), + [activeSort] ) const handleContentContextMenu = useCallback( @@ -260,12 +315,8 @@ export function Tables() { onClick: handleCreateTable, disabled: uploading || userPermissions.canEdit !== true || createTable.isPending, }} - search={{ - value: searchTerm, - onChange: setSearchTerm, - placeholder: 'Search tables...', - }} - defaultSort='created' + search={searchConfig} + sort={sortConfig} headerActions={[ { label: uploadButtonLabel, From 51c9df9b8bc58a6fefad27f12ccfccf21ff4db61 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 18:59:35 -0700 Subject: [PATCH 11/30] feat(knowledge): add content and owner filters to KB list --- .../[workspaceId]/knowledge/knowledge.tsx | 132 ++++++++++++++++-- 1 file changed, 119 insertions(+), 13 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx index ba1d724fdb4..c13069f1e7e 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx @@ -107,6 +107,8 @@ export function Knowledge() { direction: 'asc' | 'desc' } | null>(null) const [connectorFilter, setConnectorFilter] = useState<'all' | 'connected' | 'unconnected'>('all') + const [contentFilter, setContentFilter] = useState<'all' | 'has-docs' | 'empty'>('all') + const [ownerFilter, setOwnerFilter] = useState([]) const [searchInputValue, setSearchInputValue] = useState('') const debouncedSearchQuery = useDebounce(searchInputValue, 300) @@ -192,6 +194,18 @@ export function Knowledge() { ) } + if (contentFilter !== 'all') { + result = result.filter((kb) => + contentFilter === 'has-docs' + ? ((kb as KnowledgeBaseWithDocCount).docCount ?? 0) > 0 + : ((kb as KnowledgeBaseWithDocCount).docCount ?? 0) === 0 + ) + } + + if (ownerFilter.length > 0) { + result = result.filter((kb) => ownerFilter.includes(kb.userId)) + } + const col = activeSort?.column ?? 'created' const dir = activeSort?.direction ?? 'desc' return [...result].sort((a, b) => { @@ -217,7 +231,14 @@ export function Knowledge() { } return dir === 'asc' ? cmp : -cmp }) - }, [knowledgeBases, debouncedSearchQuery, connectorFilter, activeSort]) + }, [ + knowledgeBases, + debouncedSearchQuery, + connectorFilter, + contentFilter, + ownerFilter, + activeSort, + ]) const rows: ResourceRow[] = useMemo( () => @@ -375,21 +396,106 @@ export function Knowledge() { ))}
+
+ Content +
+
+ {( + [ + { value: 'all', label: 'All' }, + { value: 'has-docs', label: 'Has documents' }, + { value: 'empty', label: 'Empty' }, + ] as const + ).map(({ value, label }) => ( + + ))} +
+ {members && members.length > 0 && ( + <> +
+ Owner +
+
+ + {members.map((member) => ( + + ))} +
+ + )}
) - const filterTags: FilterTag[] = useMemo( - () => - connectorFilter === 'all' - ? [] - : [ - { - label: connectorFilter === 'connected' ? 'Connectors: Active' : 'Connectors: None', - onRemove: () => setConnectorFilter('all'), - }, - ], - [connectorFilter] - ) + const filterTags: FilterTag[] = useMemo(() => { + const tags: FilterTag[] = [] + if (connectorFilter !== 'all') { + tags.push({ + label: connectorFilter === 'connected' ? 'Connectors: Active' : 'Connectors: None', + onRemove: () => setConnectorFilter('all'), + }) + } + if (contentFilter !== 'all') { + tags.push({ + label: contentFilter === 'has-docs' ? 'Content: Has documents' : 'Content: Empty', + onRemove: () => setContentFilter('all'), + }) + } + if (ownerFilter.length > 0) { + const label = + ownerFilter.length === 1 + ? `Owner: ${members?.find((m) => m.userId === ownerFilter[0])?.name ?? '1 member'}` + : `Owner: ${ownerFilter.length} members` + tags.push({ label, onRemove: () => setOwnerFilter([]) }) + } + return tags + }, [connectorFilter, contentFilter, ownerFilter, members]) return ( <> From 12ea734149f34005f9789c3299c72ee12aee9ae8 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 19:00:01 -0700 Subject: [PATCH 12/30] feat(scheduled-tasks): add status and health filters --- .../scheduled-tasks/scheduled-tasks.tsx | 102 +++++++++++++++--- 1 file changed, 89 insertions(+), 13 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx index 172102c3d42..5a6d254351b 100644 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx +++ b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx @@ -85,6 +85,8 @@ export function ScheduledTasks() { direction: 'asc' | 'desc' } | null>(null) const [scheduleTypeFilter, setScheduleTypeFilter] = useState<'all' | 'recurring' | 'once'>('all') + const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'paused'>('all') + const [healthFilter, setHealthFilter] = useState<'all' | 'has-failures'>('all') const visibleItems = useMemo( () => allItems.filter((item) => item.sourceType === 'job' && item.status !== 'completed'), @@ -108,6 +110,16 @@ export function ScheduledTasks() { ) } + if (statusFilter !== 'all') { + result = result.filter((item) => + statusFilter === 'active' ? item.status === 'active' : item.status === 'disabled' + ) + } + + if (healthFilter === 'has-failures') { + result = result.filter((item) => (item.failedCount ?? 0) > 0) + } + const col = activeSort?.column ?? 'nextRun' const dir = activeSort?.direction ?? 'desc' return [...result].sort((a, b) => { @@ -129,7 +141,14 @@ export function ScheduledTasks() { } return dir === 'asc' ? cmp : -cmp }) - }, [visibleItems, debouncedSearchQuery, scheduleTypeFilter, activeSort]) + }, [ + visibleItems, + debouncedSearchQuery, + scheduleTypeFilter, + statusFilter, + healthFilter, + activeSort, + ]) const rows: ResourceRow[] = useMemo( () => @@ -246,21 +265,78 @@ export function ScheduledTasks() { ))}
+
+ Status +
+
+ {( + [ + { value: 'all', label: 'All' }, + { value: 'active', label: 'Active' }, + { value: 'paused', label: 'Paused' }, + ] as const + ).map(({ value, label }) => ( + + ))} +
+
+ Health +
+
+ {( + [ + { value: 'all', label: 'All' }, + { value: 'has-failures', label: 'Has failures' }, + ] as const + ).map(({ value, label }) => ( + + ))} +
) - const filterTags: FilterTag[] = useMemo( - () => - scheduleTypeFilter === 'all' - ? [] - : [ - { - label: scheduleTypeFilter === 'recurring' ? 'Type: Recurring' : 'Type: One-time', - onRemove: () => setScheduleTypeFilter('all'), - }, - ], - [scheduleTypeFilter] - ) + const filterTags: FilterTag[] = useMemo(() => { + const tags: FilterTag[] = [] + if (scheduleTypeFilter !== 'all') { + tags.push({ + label: scheduleTypeFilter === 'recurring' ? 'Type: Recurring' : 'Type: One-time', + onRemove: () => setScheduleTypeFilter('all'), + }) + } + if (statusFilter !== 'all') { + tags.push({ + label: statusFilter === 'active' ? 'Status: Active' : 'Status: Paused', + onRemove: () => setStatusFilter('all'), + }) + } + if (healthFilter === 'has-failures') { + tags.push({ + label: 'Health: Has failures', + onRemove: () => setHealthFilter('all'), + }) + } + return tags + }, [scheduleTypeFilter, statusFilter, healthFilter]) return ( <> From a3ffc2f659e464f1f86133060550279285682451 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 19:00:18 -0700 Subject: [PATCH 13/30] feat(files): add size and uploaded-by filters to files list --- .../workspace/[workspaceId]/files/files.tsx | 129 +++++++++++++++--- 1 file changed, 111 insertions(+), 18 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index f458ffc2af6..2108d5512fb 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -174,6 +174,8 @@ export function Files() { direction: 'asc' | 'desc' } | null>(null) const [typeFilter, setTypeFilter] = useState<'all' | 'document' | 'audio' | 'video'>('all') + const [sizeFilter, setSizeFilter] = useState<'all' | 'small' | 'medium' | 'large'>('all') + const [uploadedByFilter, setUploadedByFilter] = useState([]) const [creatingFile, setCreatingFile] = useState(false) const [isDirty, setIsDirty] = useState(false) @@ -223,6 +225,18 @@ export function Files() { }) } + if (sizeFilter !== 'all') { + result = result.filter((f) => { + if (sizeFilter === 'small') return f.size < 1_048_576 + if (sizeFilter === 'medium') return f.size >= 1_048_576 && f.size <= 10_485_760 + return f.size > 10_485_760 // large + }) + } + + if (uploadedByFilter.length > 0) { + result = result.filter((f) => uploadedByFilter.includes(f.uploadedBy)) + } + const col = activeSort?.column ?? 'created' const dir = activeSort?.direction ?? 'desc' return [...result].sort((a, b) => { @@ -244,7 +258,7 @@ export function Files() { } return dir === 'asc' ? cmp : -cmp }) - }, [files, debouncedSearchTerm, typeFilter, activeSort]) + }, [files, debouncedSearchTerm, typeFilter, sizeFilter, uploadedByFilter, activeSort]) const rowCacheRef = useRef( new Map() @@ -831,26 +845,105 @@ export function Files() { ))}
+
+ Size +
+
+ {( + [ + { value: 'all', label: 'All' }, + { value: 'small', label: 'Small (< 1 MB)' }, + { value: 'medium', label: 'Medium (1–10 MB)' }, + { value: 'large', label: 'Large (> 10 MB)' }, + ] as const + ).map(({ value, label }) => ( + + ))} +
+ {members && members.length > 0 && ( + <> +
+ + Uploaded By + +
+
+ + {members.map((member) => ( + + ))} +
+ + )}
) - const filterTags: FilterTag[] = useMemo( - () => - typeFilter === 'all' - ? [] - : [ - { - label: - typeFilter === 'document' - ? 'Type: Documents' - : typeFilter === 'audio' - ? 'Type: Audio' - : 'Type: Video', - onRemove: () => setTypeFilter('all'), - }, - ], - [typeFilter] - ) + const filterTags: FilterTag[] = useMemo(() => { + const tags: FilterTag[] = [] + if (typeFilter !== 'all') { + const labels = { document: 'Type: Documents', audio: 'Type: Audio', video: 'Type: Video' } + tags.push({ label: labels[typeFilter], onRemove: () => setTypeFilter('all') }) + } + if (sizeFilter !== 'all') { + const labels = { small: 'Size: Small', medium: 'Size: Medium', large: 'Size: Large' } + tags.push({ label: labels[sizeFilter], onRemove: () => setSizeFilter('all') }) + } + if (uploadedByFilter.length > 0) { + const label = + uploadedByFilter.length === 1 + ? `Uploaded by: ${members?.find((m) => m.userId === uploadedByFilter[0])?.name ?? '1 member'}` + : `Uploaded by: ${uploadedByFilter.length} members` + tags.push({ label, onRemove: () => setUploadedByFilter([]) }) + } + return tags + }, [typeFilter, sizeFilter, uploadedByFilter, members]) if (fileIdFromRoute && !selectedFile) { return ( From 0142c69804f5152cef01e2af6b0aa235d8d3892e Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 19:01:24 -0700 Subject: [PATCH 14/30] feat(tables): add row count, owner, and column type filters --- .../workspace/[workspaceId]/tables/tables.tsx | 170 +++++++++++++++++- 1 file changed, 168 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx b/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx index 33ca3ca85bd..e580d99c983 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx @@ -16,7 +16,9 @@ import { import { Columns3, Rows3, Table as TableIcon } from '@/components/emcn/icons' import type { TableDefinition } from '@/lib/table' import { generateUniqueTableName } from '@/lib/table/constants' +import { cn } from '@/lib/utils' import type { + FilterTag, ResourceColumn, ResourceRow, SearchConfig, @@ -47,6 +49,14 @@ const COLUMNS: ResourceColumn[] = [ { id: 'updated', header: 'Last Updated' }, ] +const COLUMN_TYPE_LABELS: Record = { + string: 'Text', + number: 'Number', + boolean: 'Boolean', + date: 'Date', + json: 'JSON', +} + export function Tables() { const params = useParams() const router = useRouter() @@ -71,6 +81,9 @@ export function Tables() { column: string direction: 'asc' | 'desc' } | null>(null) + const [rowCountFilter, setRowCountFilter] = useState<'all' | 'empty' | 'small' | 'large'>('all') + const [ownerFilter, setOwnerFilter] = useState([]) + const [columnTypeFilter, setColumnTypeFilter] = useState([]) const [uploading, setUploading] = useState(false) const [uploadProgress, setUploadProgress] = useState({ completed: 0, total: 0 }) const csvInputRef = useRef(null) @@ -90,10 +103,26 @@ export function Tables() { } = useContextMenu() const processedTables = useMemo(() => { - const result = debouncedSearchTerm + let result = debouncedSearchTerm ? tables.filter((t) => t.name.toLowerCase().includes(debouncedSearchTerm.toLowerCase())) : tables + if (rowCountFilter !== 'all') { + result = result.filter((t) => { + if (rowCountFilter === 'empty') return t.rowCount === 0 + if (rowCountFilter === 'small') return t.rowCount >= 1 && t.rowCount <= 100 + return t.rowCount > 100 // large + }) + } + if (ownerFilter.length > 0) { + result = result.filter((t) => ownerFilter.includes(t.createdBy)) + } + if (columnTypeFilter.length > 0) { + result = result.filter((t) => + t.schema.columns.some((col) => columnTypeFilter.includes(col.type)) + ) + } + const col = activeSort?.column ?? 'created' const dir = activeSort?.direction ?? 'desc' return [...result].sort((a, b) => { @@ -117,7 +146,7 @@ export function Tables() { } return dir === 'asc' ? cmp : -cmp }) - }, [tables, debouncedSearchTerm, activeSort]) + }, [tables, debouncedSearchTerm, activeSort, rowCountFilter, ownerFilter, columnTypeFilter]) const rows: ResourceRow[] = useMemo( () => @@ -170,6 +199,141 @@ export function Tables() { [activeSort] ) + const filterContent = ( +
+
+ Row Count +
+
+ {( + [ + { value: 'all', label: 'All' }, + { value: 'empty', label: 'Empty' }, + { value: 'small', label: 'Small (1–100 rows)' }, + { value: 'large', label: 'Large (100+ rows)' }, + ] as const + ).map(({ value, label }) => ( + + ))} +
+
+ Column Types +
+
+ + {(['string', 'number', 'boolean', 'date', 'json'] as const).map((type) => ( + + ))} +
+ {members && members.length > 0 && ( + <> +
+ Owner +
+
+ + {members.map((member) => ( + + ))} +
+ + )} +
+ ) + + const filterTags: FilterTag[] = useMemo(() => { + const tags: FilterTag[] = [] + if (rowCountFilter !== 'all') { + const labels = { empty: 'Rows: Empty', small: 'Rows: Small', large: 'Rows: Large' } + tags.push({ label: labels[rowCountFilter], onRemove: () => setRowCountFilter('all') }) + } + if (columnTypeFilter.length > 0) { + const label = + columnTypeFilter.length === 1 + ? `Type: ${COLUMN_TYPE_LABELS[columnTypeFilter[0]]}` + : `Types: ${columnTypeFilter.length} selected` + tags.push({ label, onRemove: () => setColumnTypeFilter([]) }) + } + if (ownerFilter.length > 0) { + const label = + ownerFilter.length === 1 + ? `Owner: ${members?.find((m) => m.userId === ownerFilter[0])?.name ?? '1 member'}` + : `Owner: ${ownerFilter.length} members` + tags.push({ label, onRemove: () => setOwnerFilter([]) }) + } + return tags + }, [rowCountFilter, columnTypeFilter, ownerFilter, members]) + const handleContentContextMenu = useCallback( (e: React.MouseEvent) => { const target = e.target as HTMLElement @@ -317,6 +481,8 @@ export function Tables() { }} search={searchConfig} sort={sortConfig} + filter={filterContent} + filterTags={filterTags} headerActions={[ { label: uploadButtonLabel, From 2553cac981e164d54c30fcdb358083f4b0c70202 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 19:08:02 -0700 Subject: [PATCH 15/30] improvement(scheduled-tasks): use combobox filter panel matching logs UI style --- .../scheduled-tasks/scheduled-tasks.tsx | 209 ++++++++++-------- 1 file changed, 118 insertions(+), 91 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx index 5a6d254351b..e7fb50c31ee 100644 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx +++ b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx @@ -3,9 +3,16 @@ import { useCallback, useMemo, useState } from 'react' import { createLogger } from '@sim/logger' import { useParams } from 'next/navigation' -import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn' +import { + Button, + Combobox, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, +} from '@/components/emcn' import { Calendar } from '@/components/emcn/icons' -import { cn } from '@/lib/core/utils/cn' import { formatAbsoluteDate } from '@/lib/core/utils/formatting' import { parseCronToHumanReadable } from '@/lib/workflows/schedules/utils' import type { @@ -84,9 +91,9 @@ export function ScheduledTasks() { column: string direction: 'asc' | 'desc' } | null>(null) - const [scheduleTypeFilter, setScheduleTypeFilter] = useState<'all' | 'recurring' | 'once'>('all') - const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'paused'>('all') - const [healthFilter, setHealthFilter] = useState<'all' | 'has-failures'>('all') + const [scheduleTypeFilter, setScheduleTypeFilter] = useState([]) + const [statusFilter, setStatusFilter] = useState([]) + const [healthFilter, setHealthFilter] = useState([]) const visibleItems = useMemo( () => allItems.filter((item) => item.sourceType === 'job' && item.status !== 'completed'), @@ -104,19 +111,23 @@ export function ScheduledTasks() { }) : visibleItems - if (scheduleTypeFilter !== 'all') { - result = result.filter((item) => - scheduleTypeFilter === 'recurring' ? Boolean(item.cronExpression) : !item.cronExpression - ) + if (scheduleTypeFilter.length > 0) { + result = result.filter((item) => { + if (scheduleTypeFilter.includes('recurring') && Boolean(item.cronExpression)) return true + if (scheduleTypeFilter.includes('once') && !item.cronExpression) return true + return false + }) } - if (statusFilter !== 'all') { - result = result.filter((item) => - statusFilter === 'active' ? item.status === 'active' : item.status === 'disabled' - ) + if (statusFilter.length > 0) { + result = result.filter((item) => { + if (statusFilter.includes('active') && item.status === 'active') return true + if (statusFilter.includes('paused') && item.status === 'disabled') return true + return false + }) } - if (healthFilter === 'has-failures') { + if (healthFilter.length > 0 && healthFilter.includes('has-failures')) { result = result.filter((item) => (item.failedCount ?? 0) > 0) } @@ -239,101 +250,117 @@ export function ScheduledTasks() { [activeSort] ) + const scheduleTypeDisplayLabel = useMemo(() => { + if (scheduleTypeFilter.length === 0) return 'All' + if (scheduleTypeFilter.length === 1) + return scheduleTypeFilter[0] === 'recurring' ? 'Recurring' : 'One-time' + return `${scheduleTypeFilter.length} selected` + }, [scheduleTypeFilter]) + + const statusDisplayLabel = useMemo(() => { + if (statusFilter.length === 0) return 'All' + if (statusFilter.length === 1) return statusFilter[0] === 'active' ? 'Active' : 'Paused' + return `${statusFilter.length} selected` + }, [statusFilter]) + + const healthDisplayLabel = useMemo(() => { + if (healthFilter.length === 0) return 'All' + return 'Has failures' + }, [healthFilter]) + + const hasActiveFilters = + scheduleTypeFilter.length > 0 || statusFilter.length > 0 || healthFilter.length > 0 + const filterContent = ( -
-
+
+
Schedule Type -
-
- {( - [ - { value: 'all', label: 'All' }, + ( - - ))} + ]} + multiSelect + multiSelectValues={scheduleTypeFilter} + onMultiSelectChange={setScheduleTypeFilter} + overlayContent={ + {scheduleTypeDisplayLabel} + } + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + />
-
+
Status -
-
- {( - [ - { value: 'all', label: 'All' }, + ( - - ))} + ]} + multiSelect + multiSelectValues={statusFilter} + onMultiSelectChange={setStatusFilter} + overlayContent={ + {statusDisplayLabel} + } + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + />
-
+
Health + {healthDisplayLabel} + } + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + />
-
- {( - [ - { value: 'all', label: 'All' }, - { value: 'has-failures', label: 'Has failures' }, - ] as const - ).map(({ value, label }) => ( - - ))} -
+ {hasActiveFilters && ( + + )}
) const filterTags: FilterTag[] = useMemo(() => { const tags: FilterTag[] = [] - if (scheduleTypeFilter !== 'all') { - tags.push({ - label: scheduleTypeFilter === 'recurring' ? 'Type: Recurring' : 'Type: One-time', - onRemove: () => setScheduleTypeFilter('all'), - }) + if (scheduleTypeFilter.length > 0) { + const label = + scheduleTypeFilter.length === 1 + ? `Type: ${scheduleTypeFilter[0] === 'recurring' ? 'Recurring' : 'One-time'}` + : `Type: ${scheduleTypeFilter.length} selected` + tags.push({ label, onRemove: () => setScheduleTypeFilter([]) }) } - if (statusFilter !== 'all') { - tags.push({ - label: statusFilter === 'active' ? 'Status: Active' : 'Status: Paused', - onRemove: () => setStatusFilter('all'), - }) + if (statusFilter.length > 0) { + const label = + statusFilter.length === 1 + ? `Status: ${statusFilter[0] === 'active' ? 'Active' : 'Paused'}` + : `Status: ${statusFilter.length} selected` + tags.push({ label, onRemove: () => setStatusFilter([]) }) } - if (healthFilter === 'has-failures') { - tags.push({ - label: 'Health: Has failures', - onRemove: () => setHealthFilter('all'), - }) + if (healthFilter.length > 0) { + tags.push({ label: 'Health: Has failures', onRemove: () => setHealthFilter([]) }) } return tags }, [scheduleTypeFilter, statusFilter, healthFilter]) From 9dd3028dcbd7af429062a18bc9c2ee0d0ae5f101 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 19:08:25 -0700 Subject: [PATCH 16/30] improvement(knowledge): use combobox filter panel matching logs UI style --- .../[workspaceId]/knowledge/knowledge.tsx | 254 ++++++++++-------- 1 file changed, 138 insertions(+), 116 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx index c13069f1e7e..b1d2ff40bea 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx @@ -3,9 +3,9 @@ import { useCallback, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { useParams, useRouter } from 'next/navigation' -import { Tooltip } from '@/components/emcn' +import type { ComboboxOption } from '@/components/emcn' +import { Combobox, Tooltip } from '@/components/emcn' import { Database } from '@/components/emcn/icons' -import { cn } from '@/lib/core/utils/cn' import type { KnowledgeBaseData } from '@/lib/knowledge/types' import type { CreateAction, @@ -106,8 +106,8 @@ export function Knowledge() { column: string direction: 'asc' | 'desc' } | null>(null) - const [connectorFilter, setConnectorFilter] = useState<'all' | 'connected' | 'unconnected'>('all') - const [contentFilter, setContentFilter] = useState<'all' | 'has-docs' | 'empty'>('all') + const [connectorFilter, setConnectorFilter] = useState([]) + const [contentFilter, setContentFilter] = useState([]) const [ownerFilter, setOwnerFilter] = useState([]) const [searchInputValue, setSearchInputValue] = useState('') @@ -186,20 +186,22 @@ export function Knowledge() { const processedKBs = useMemo(() => { let result = filterKnowledgeBases(knowledgeBases, debouncedSearchQuery) - if (connectorFilter !== 'all') { - result = result.filter((kb) => - connectorFilter === 'connected' - ? (kb.connectorTypes?.length ?? 0) > 0 - : (kb.connectorTypes?.length ?? 0) === 0 - ) + if (connectorFilter.length > 0) { + result = result.filter((kb) => { + const hasConnectors = (kb.connectorTypes?.length ?? 0) > 0 + if (connectorFilter.includes('connected') && hasConnectors) return true + if (connectorFilter.includes('unconnected') && !hasConnectors) return true + return false + }) } - if (contentFilter !== 'all') { - result = result.filter((kb) => - contentFilter === 'has-docs' - ? ((kb as KnowledgeBaseWithDocCount).docCount ?? 0) > 0 - : ((kb as KnowledgeBaseWithDocCount).docCount ?? 0) === 0 - ) + if (contentFilter.length > 0) { + const docCount = (kb: KnowledgeBaseData) => (kb as KnowledgeBaseWithDocCount).docCount ?? 0 + result = result.filter((kb) => { + if (contentFilter.includes('has-docs') && docCount(kb) > 0) return true + if (contentFilter.includes('empty') && docCount(kb) === 0) return true + return false + }) } if (ownerFilter.length > 0) { @@ -370,122 +372,142 @@ export function Knowledge() { [activeSort] ) + const connectorDisplayLabel = useMemo(() => { + if (connectorFilter.length === 0) return 'All' + if (connectorFilter.length === 1) + return connectorFilter[0] === 'connected' ? 'With connectors' : 'Without connectors' + return `${connectorFilter.length} selected` + }, [connectorFilter]) + + const contentDisplayLabel = useMemo(() => { + if (contentFilter.length === 0) return 'All' + if (contentFilter.length === 1) + return contentFilter[0] === 'has-docs' ? 'Has documents' : 'Empty' + return `${contentFilter.length} selected` + }, [contentFilter]) + + const ownerDisplayLabel = useMemo(() => { + if (ownerFilter.length === 0) return 'All' + if (ownerFilter.length === 1) + return members?.find((m) => m.userId === ownerFilter[0])?.name ?? '1 member' + return `${ownerFilter.length} members` + }, [ownerFilter, members]) + + const memberOptions: ComboboxOption[] = useMemo( + () => + (members ?? []).map((m) => ({ + value: m.userId, + label: m.name, + iconElement: m.image ? ( + {m.name} + ) : ( + + {m.name.charAt(0).toUpperCase()} + + ), + })), + [members] + ) + + const hasActiveFilters = + connectorFilter.length > 0 || contentFilter.length > 0 || ownerFilter.length > 0 + const filterContent = ( -
-
+
+
Connectors -
-
- {( - [ - { value: 'all', label: 'All' }, + ( - - ))} + ]} + multiSelect + multiSelectValues={connectorFilter} + onMultiSelectChange={setConnectorFilter} + overlayContent={ + {connectorDisplayLabel} + } + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + />
-
+
Content -
-
- {( - [ - { value: 'all', label: 'All' }, + ( - - ))} + ]} + multiSelect + multiSelectValues={contentFilter} + onMultiSelectChange={setContentFilter} + overlayContent={ + {contentDisplayLabel} + } + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + />
- {members && members.length > 0 && ( - <> -
- Owner -
-
- - {members.map((member) => ( - - ))} -
- + {memberOptions.length > 0 && ( +
+ Owner + {ownerDisplayLabel} + } + searchable + searchPlaceholder='Search members...' + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + /> +
+ )} + {hasActiveFilters && ( + )}
) const filterTags: FilterTag[] = useMemo(() => { const tags: FilterTag[] = [] - if (connectorFilter !== 'all') { - tags.push({ - label: connectorFilter === 'connected' ? 'Connectors: Active' : 'Connectors: None', - onRemove: () => setConnectorFilter('all'), - }) + if (connectorFilter.length > 0) { + const label = + connectorFilter.length === 1 + ? `Connectors: ${connectorFilter[0] === 'connected' ? 'With connectors' : 'Without connectors'}` + : `Connectors: ${connectorFilter.length} types` + tags.push({ label, onRemove: () => setConnectorFilter([]) }) } - if (contentFilter !== 'all') { - tags.push({ - label: contentFilter === 'has-docs' ? 'Content: Has documents' : 'Content: Empty', - onRemove: () => setContentFilter('all'), - }) + if (contentFilter.length > 0) { + const label = + contentFilter.length === 1 + ? `Content: ${contentFilter[0] === 'has-docs' ? 'Has documents' : 'Empty'}` + : `Content: ${contentFilter.length} types` + tags.push({ label, onRemove: () => setContentFilter([]) }) } if (ownerFilter.length > 0) { const label = From 446a6650143ce33de9d2e1fa86efe2c2c83aa751 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 19:08:55 -0700 Subject: [PATCH 17/30] improvement(files): use combobox filter panel matching logs UI style Replaces button-list filters with Combobox-based multi-select sections for file type, size, and uploaded-by filters, aligning the panel with the logs page filter UI. --- .../workspace/[workspaceId]/files/files.tsx | 262 ++++++++++-------- 1 file changed, 152 insertions(+), 110 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 2108d5512fb..f02614cac43 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -6,6 +6,7 @@ import { useParams, useRouter } from 'next/navigation' import { Button, Columns2, + type ComboboxOption, Download, DropdownMenu, DropdownMenuContent, @@ -25,7 +26,6 @@ import { } from '@/components/emcn' import { File as FilesIcon } from '@/components/emcn/icons' import { getDocumentIcon } from '@/components/icons/document-icons' -import { cn } from '@/lib/core/utils/cn' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' import { downloadWorkspaceFile, @@ -173,8 +173,8 @@ export function Files() { column: string direction: 'asc' | 'desc' } | null>(null) - const [typeFilter, setTypeFilter] = useState<'all' | 'document' | 'audio' | 'video'>('all') - const [sizeFilter, setSizeFilter] = useState<'all' | 'small' | 'medium' | 'large'>('all') + const [typeFilter, setTypeFilter] = useState([]) + const [sizeFilter, setSizeFilter] = useState([]) const [uploadedByFilter, setUploadedByFilter] = useState([]) const [creatingFile, setCreatingFile] = useState(false) @@ -215,21 +215,23 @@ export function Files() { ? files.filter((f) => f.name.toLowerCase().includes(debouncedSearchTerm.toLowerCase())) : files - if (typeFilter !== 'all') { + if (typeFilter.length > 0) { result = result.filter((f) => { const ext = getFileExtension(f.name) - if (typeFilter === 'document') return isSupportedExtension(ext) - if (typeFilter === 'audio') return isSupportedAudioExtension(ext) - if (typeFilter === 'video') return isSupportedVideoExtension(ext) - return true + if (typeFilter.includes('document') && isSupportedExtension(ext)) return true + if (typeFilter.includes('audio') && isSupportedAudioExtension(ext)) return true + if (typeFilter.includes('video') && isSupportedVideoExtension(ext)) return true + return false }) } - if (sizeFilter !== 'all') { + if (sizeFilter.length > 0) { result = result.filter((f) => { - if (sizeFilter === 'small') return f.size < 1_048_576 - if (sizeFilter === 'medium') return f.size >= 1_048_576 && f.size <= 10_485_760 - return f.size > 10_485_760 // large + if (sizeFilter.includes('small') && f.size < 1_048_576) return true + if (sizeFilter.includes('medium') && f.size >= 1_048_576 && f.size <= 10_485_760) + return true + if (sizeFilter.includes('large') && f.size > 10_485_760) return true + return false }) } @@ -803,6 +805,56 @@ export function Files() { [handleNavigateToFiles] ) + const typeDisplayLabel = useMemo(() => { + if (typeFilter.length === 0) return 'All' + if (typeFilter.length === 1) { + const labels: Record = { + document: 'Documents', + audio: 'Audio', + video: 'Video', + } + return labels[typeFilter[0]] ?? typeFilter[0] + } + return `${typeFilter.length} selected` + }, [typeFilter]) + + const sizeDisplayLabel = useMemo(() => { + if (sizeFilter.length === 0) return 'All' + if (sizeFilter.length === 1) { + const labels: Record = { small: 'Small', medium: 'Medium', large: 'Large' } + return labels[sizeFilter[0]] ?? sizeFilter[0] + } + return `${sizeFilter.length} selected` + }, [sizeFilter]) + + const uploadedByDisplayLabel = useMemo(() => { + if (uploadedByFilter.length === 0) return 'All' + if (uploadedByFilter.length === 1) + return members?.find((m) => m.userId === uploadedByFilter[0])?.name ?? '1 member' + return `${uploadedByFilter.length} members` + }, [uploadedByFilter, members]) + + const memberOptions: ComboboxOption[] = useMemo( + () => + (members ?? []).map((m) => ({ + value: m.userId, + label: m.name, + iconElement: m.image ? ( + {m.name} + ) : ( + + {m.name.charAt(0).toUpperCase()} + + ), + })), + [members] + ) + const sortConfig: SortConfig = useMemo( () => ({ options: [ @@ -818,122 +870,112 @@ export function Files() { [activeSort] ) + const hasActiveFilters = + typeFilter.length > 0 || sizeFilter.length > 0 || uploadedByFilter.length > 0 + const filterContent = ( -
-
+
+
File Type -
-
- {( - [ - { value: 'all', label: 'All' }, + ( - - ))} + ]} + multiSelect + multiSelectValues={typeFilter} + onMultiSelectChange={setTypeFilter} + overlayContent={ + {typeDisplayLabel} + } + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + />
-
+
Size -
-
- {( - [ - { value: 'all', label: 'All' }, + 10 MB)' }, - ] as const - ).map(({ value, label }) => ( - - ))} + ]} + multiSelect + multiSelectValues={sizeFilter} + onMultiSelectChange={setSizeFilter} + overlayContent={ + {sizeDisplayLabel} + } + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + />
- {members && members.length > 0 && ( - <> -
- - Uploaded By - -
-
- - {members.map((member) => ( - - ))} -
- + {memberOptions.length > 0 && ( +
+ Uploaded By + {uploadedByDisplayLabel} + } + searchable + searchPlaceholder='Search members...' + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + /> +
+ )} + {hasActiveFilters && ( + )}
) const filterTags: FilterTag[] = useMemo(() => { const tags: FilterTag[] = [] - if (typeFilter !== 'all') { - const labels = { document: 'Type: Documents', audio: 'Type: Audio', video: 'Type: Video' } - tags.push({ label: labels[typeFilter], onRemove: () => setTypeFilter('all') }) + if (typeFilter.length > 0) { + const typeLabels: Record = { + document: 'Documents', + audio: 'Audio', + video: 'Video', + } + const label = + typeFilter.length === 1 + ? `Type: ${typeLabels[typeFilter[0]]}` + : `Type: ${typeFilter.length} selected` + tags.push({ label, onRemove: () => setTypeFilter([]) }) } - if (sizeFilter !== 'all') { - const labels = { small: 'Size: Small', medium: 'Size: Medium', large: 'Size: Large' } - tags.push({ label: labels[sizeFilter], onRemove: () => setSizeFilter('all') }) + if (sizeFilter.length > 0) { + const sizeLabels: Record = { + small: 'Small', + medium: 'Medium', + large: 'Large', + } + const label = + sizeFilter.length === 1 + ? `Size: ${sizeLabels[sizeFilter[0]]}` + : `Size: ${sizeFilter.length} selected` + tags.push({ label, onRemove: () => setSizeFilter([]) }) } if (uploadedByFilter.length > 0) { const label = From c56e3ac855e7c40ac6c3b19663fa1310145c9a9d Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 19:10:07 -0700 Subject: [PATCH 18/30] improvement(tables): use combobox filter panel matching logs UI style --- .../workspace/[workspaceId]/tables/tables.tsx | 262 ++++++++++-------- 1 file changed, 147 insertions(+), 115 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx b/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx index e580d99c983..7d7604c4ada 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx @@ -3,8 +3,10 @@ import { useCallback, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { useParams, useRouter } from 'next/navigation' +import type { ComboboxOption } from '@/components/emcn' import { Button, + Combobox, Modal, ModalBody, ModalContent, @@ -16,7 +18,6 @@ import { import { Columns3, Rows3, Table as TableIcon } from '@/components/emcn/icons' import type { TableDefinition } from '@/lib/table' import { generateUniqueTableName } from '@/lib/table/constants' -import { cn } from '@/lib/utils' import type { FilterTag, ResourceColumn, @@ -49,14 +50,6 @@ const COLUMNS: ResourceColumn[] = [ { id: 'updated', header: 'Last Updated' }, ] -const COLUMN_TYPE_LABELS: Record = { - string: 'Text', - number: 'Number', - boolean: 'Boolean', - date: 'Date', - json: 'JSON', -} - export function Tables() { const params = useParams() const router = useRouter() @@ -81,7 +74,7 @@ export function Tables() { column: string direction: 'asc' | 'desc' } | null>(null) - const [rowCountFilter, setRowCountFilter] = useState<'all' | 'empty' | 'small' | 'large'>('all') + const [rowCountFilter, setRowCountFilter] = useState([]) const [ownerFilter, setOwnerFilter] = useState([]) const [columnTypeFilter, setColumnTypeFilter] = useState([]) const [uploading, setUploading] = useState(false) @@ -107,11 +100,12 @@ export function Tables() { ? tables.filter((t) => t.name.toLowerCase().includes(debouncedSearchTerm.toLowerCase())) : tables - if (rowCountFilter !== 'all') { + if (rowCountFilter.length > 0) { result = result.filter((t) => { - if (rowCountFilter === 'empty') return t.rowCount === 0 - if (rowCountFilter === 'small') return t.rowCount >= 1 && t.rowCount <= 100 - return t.rowCount > 100 // large + if (rowCountFilter.includes('empty') && t.rowCount === 0) return true + if (rowCountFilter.includes('small') && t.rowCount >= 1 && t.rowCount <= 100) return true + if (rowCountFilter.includes('large') && t.rowCount > 100) return true + return false }) } if (ownerFilter.length > 0) { @@ -146,7 +140,7 @@ export function Tables() { } return dir === 'asc' ? cmp : -cmp }) - }, [tables, debouncedSearchTerm, activeSort, rowCountFilter, ownerFilter, columnTypeFilter]) + }, [tables, debouncedSearchTerm, rowCountFilter, ownerFilter, columnTypeFilter, activeSort]) const rows: ResourceRow[] = useMemo( () => @@ -199,128 +193,166 @@ export function Tables() { [activeSort] ) + const rowCountDisplayLabel = useMemo(() => { + if (rowCountFilter.length === 0) return 'All' + if (rowCountFilter.length === 1) { + const labels: Record = { + empty: 'Empty', + small: 'Small (1–100)', + large: 'Large (100+)', + } + return labels[rowCountFilter[0]] ?? rowCountFilter[0] + } + return `${rowCountFilter.length} selected` + }, [rowCountFilter]) + + const columnTypeDisplayLabel = useMemo(() => { + if (columnTypeFilter.length === 0) return 'All' + if (columnTypeFilter.length === 1) { + const labels: Record = { + string: 'Text', + number: 'Number', + boolean: 'Boolean', + date: 'Date', + json: 'JSON', + } + return labels[columnTypeFilter[0]] ?? columnTypeFilter[0] + } + return `${columnTypeFilter.length} selected` + }, [columnTypeFilter]) + + const ownerDisplayLabel = useMemo(() => { + if (ownerFilter.length === 0) return 'All' + if (ownerFilter.length === 1) + return members?.find((m) => m.userId === ownerFilter[0])?.name ?? '1 member' + return `${ownerFilter.length} members` + }, [ownerFilter, members]) + + const memberOptions: ComboboxOption[] = useMemo( + () => + (members ?? []).map((m) => ({ + value: m.userId, + label: m.name, + iconElement: m.image ? ( + {m.name} + ) : ( + + {m.name.charAt(0).toUpperCase()} + + ), + })), + [members] + ) + + const hasActiveFilters = + rowCountFilter.length > 0 || columnTypeFilter.length > 0 || ownerFilter.length > 0 + const filterContent = ( -
-
+
+
Row Count -
-
- {( - [ - { value: 'all', label: 'All' }, + ( - - ))} + ]} + multiSelect + multiSelectValues={rowCountFilter} + onMultiSelectChange={setRowCountFilter} + overlayContent={ + {rowCountDisplayLabel} + } + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + />
-
+
Column Types + {columnTypeDisplayLabel} + } + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + />
-
+ {memberOptions.length > 0 && ( +
+ Owner + {ownerDisplayLabel} + } + searchable + searchPlaceholder='Search members...' + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + /> +
+ )} + {hasActiveFilters && ( - {(['string', 'number', 'boolean', 'date', 'json'] as const).map((type) => ( - - ))} -
- {members && members.length > 0 && ( - <> -
- Owner -
-
- - {members.map((member) => ( - - ))} -
- )}
) const filterTags: FilterTag[] = useMemo(() => { const tags: FilterTag[] = [] - if (rowCountFilter !== 'all') { - const labels = { empty: 'Rows: Empty', small: 'Rows: Small', large: 'Rows: Large' } - tags.push({ label: labels[rowCountFilter], onRemove: () => setRowCountFilter('all') }) + if (rowCountFilter.length > 0) { + const rowLabels: Record = { empty: 'Empty', small: 'Small', large: 'Large' } + const label = + rowCountFilter.length === 1 + ? `Rows: ${rowLabels[rowCountFilter[0]]}` + : `Rows: ${rowCountFilter.length} selected` + tags.push({ label, onRemove: () => setRowCountFilter([]) }) } if (columnTypeFilter.length > 0) { + const typeLabels: Record = { + string: 'Text', + number: 'Number', + boolean: 'Boolean', + date: 'Date', + json: 'JSON', + } const label = columnTypeFilter.length === 1 - ? `Type: ${COLUMN_TYPE_LABELS[columnTypeFilter[0]]}` + ? `Type: ${typeLabels[columnTypeFilter[0]]}` : `Types: ${columnTypeFilter.length} selected` tags.push({ label, onRemove: () => setColumnTypeFilter([]) }) } From f0e988d7255f27b2e673ba848647bcbd03ffa235 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 19:14:51 -0700 Subject: [PATCH 19/30] feat(settings): add sort to recently deleted page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a sort dropdown next to the search bar allowing users to sort by deletion date (default, newest first), name (A–Z), or type (A–Z). --- .../recently-deleted/recently-deleted.tsx | 110 +++++++++++++++--- 1 file changed, 94 insertions(+), 16 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx index 07050c27595..e89b53797bb 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx @@ -3,7 +3,17 @@ import { useMemo, useState } from 'react' import { Search } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' -import { Button, SModalTabs, SModalTabsList, SModalTabsTrigger } from '@/components/emcn' +import { + ArrowUpDown, + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + SModalTabs, + SModalTabsList, + SModalTabsTrigger, +} from '@/components/emcn' import { Input } from '@/components/ui' import { formatDate } from '@/lib/core/utils/formatting' import { RESOURCE_REGISTRY } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry' @@ -34,6 +44,19 @@ function getResourceHref( type ResourceType = 'all' | 'workflow' | 'table' | 'knowledge' | 'file' +type SortColumn = 'deleted' | 'name' | 'type' + +interface SortConfig { + column: SortColumn + direction: 'asc' | 'desc' +} + +const SORT_OPTIONS: { column: SortColumn; direction: 'asc' | 'desc'; label: string }[] = [ + { column: 'deleted', direction: 'desc', label: 'Deleted (newest first)' }, + { column: 'name', direction: 'asc', label: 'Name (A–Z)' }, + { column: 'type', direction: 'asc', label: 'Type (A–Z)' }, +] + const ICON_CLASS = 'h-[14px] w-[14px]' const RESOURCE_TYPE_TO_MOTHERSHIP: Record, MothershipResourceType> = { @@ -100,6 +123,7 @@ export function RecentlyDeleted() { const workspaceId = params?.workspaceId as string const [activeTab, setActiveTab] = useState('all') const [searchTerm, setSearchTerm] = useState('') + const [activeSort, setActiveSort] = useState(null) const [restoringIds, setRestoringIds] = useState>(new Set()) const [restoredItems, setRestoredItems] = useState>(new Map()) @@ -174,7 +198,6 @@ export function RecentlyDeleted() { } } - items.sort((a, b) => b.deletedAt.getTime() - a.deletedAt.getTime()) return items }, [ workflowsQuery.data, @@ -191,8 +214,24 @@ export function RecentlyDeleted() { const normalized = searchTerm.toLowerCase() items = items.filter((r) => r.name.toLowerCase().includes(normalized)) } - return items - }, [resources, activeTab, searchTerm]) + const col = activeSort?.column ?? 'deleted' + const dir = activeSort?.direction ?? 'desc' + return [...items].sort((a, b) => { + let cmp = 0 + switch (col) { + case 'name': + cmp = a.name.localeCompare(b.name) + break + case 'type': + cmp = a.type.localeCompare(b.type) + break + case 'deleted': + cmp = a.deletedAt.getTime() - b.deletedAt.getTime() + break + } + return dir === 'asc' ? cmp : -cmp + }) + }, [resources, activeTab, searchTerm, activeSort]) const showNoResults = searchTerm.trim() && filtered.length === 0 && resources.length > 0 @@ -232,18 +271,57 @@ export function RecentlyDeleted() { return (
-
- - setSearchTerm(e.target.value)} - disabled={isLoading} - className='h-auto flex-1 border-0 bg-transparent p-0 font-base leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' - /> +
+
+ + setSearchTerm(e.target.value)} + disabled={isLoading} + className='h-auto flex-1 border-0 bg-transparent p-0 font-base leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' + /> +
+ + + + + + {SORT_OPTIONS.map((option) => { + const isActive = + (activeSort?.column ?? 'deleted') === option.column && + (activeSort?.direction ?? 'desc') === option.direction + return ( + + setActiveSort({ column: option.column, direction: option.direction }) + } + className={isActive ? 'bg-[var(--bg-selected)]' : ''} + > + {option.label} + + ) + })} + +
setActiveTab(v as ResourceType)}> From bf0bcf33863497d86aedd8a16352c0fa205fd8e9 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 19:15:16 -0700 Subject: [PATCH 20/30] feat(logs): add sort to logs page --- .../app/workspace/[workspaceId]/logs/logs.tsx | 70 ++++++++++++++++--- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index e95b32a3554..fc96f4ddb11 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -288,6 +288,10 @@ export default function Logs() { const activeLogRefetchRef = useRef<() => void>(() => {}) const logsQueryRef = useRef({ isFetching: false, hasNextPage: false, fetchNextPage: () => {} }) const [isNotificationSettingsOpen, setIsNotificationSettingsOpen] = useState(false) + const [activeSort, setActiveSort] = useState<{ + column: string + direction: 'asc' | 'desc' + } | null>(null) const userPermissions = useUserPermissionsContext() const [contextMenuOpen, setContextMenuOpen] = useState(false) @@ -358,11 +362,43 @@ export default function Logs() { return logsQuery.data.pages.flatMap((page) => page.logs) }, [logsQuery.data?.pages]) + const sortedLogs = useMemo(() => { + if (!activeSort) return logs + + const { column, direction } = activeSort + return [...logs].sort((a, b) => { + let cmp = 0 + switch (column) { + case 'date': + cmp = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + break + case 'duration': { + const aDuration = parseDuration({ duration: a.duration ?? undefined }) ?? -1 + const bDuration = parseDuration({ duration: b.duration ?? undefined }) ?? -1 + cmp = aDuration - bDuration + break + } + case 'cost': { + const aCost = typeof a.cost?.total === 'number' ? a.cost.total : -1 + const bCost = typeof b.cost?.total === 'number' ? b.cost.total : -1 + cmp = aCost - bCost + break + } + case 'status': + cmp = (a.status ?? '').localeCompare(b.status ?? '') + break + default: + break + } + return direction === 'asc' ? cmp : -cmp + }) + }, [logs, activeSort]) + const selectedLogIndex = useMemo( - () => (selectedLogId ? logs.findIndex((l) => l.id === selectedLogId) : -1), - [logs, selectedLogId] + () => (selectedLogId ? sortedLogs.findIndex((l) => l.id === selectedLogId) : -1), + [sortedLogs, selectedLogId] ) - const selectedLogFromList = selectedLogIndex >= 0 ? logs[selectedLogIndex] : null + const selectedLogFromList = selectedLogIndex >= 0 ? sortedLogs[selectedLogIndex] : null const selectedLog = useMemo(() => { if (!selectedLogFromList) return null @@ -381,8 +417,8 @@ export default function Logs() { useFolders(workspaceId) useEffect(() => { - logsRef.current = logs - }, [logs]) + logsRef.current = sortedLogs + }, [sortedLogs]) useEffect(() => { selectedLogIndexRef.current = selectedLogIndex }, [selectedLogIndex]) @@ -659,7 +695,7 @@ export default function Logs() { const rows: ResourceRow[] = useMemo( () => - logs.map((log) => { + sortedLogs.map((log) => { const formattedDate = formatDate(log.createdAt) const displayStatus = getDisplayStatus(log.status) const isMothershipJob = log.trigger === 'mothership' @@ -710,7 +746,7 @@ export default function Logs() { }, } }), - [logs] + [sortedLogs] ) const sidebarOverlay = useMemo( @@ -721,7 +757,7 @@ export default function Logs() { onClose={handleCloseSidebar} onNavigateNext={handleNavigateNext} onNavigatePrev={handleNavigatePrev} - hasNext={selectedLogIndex < logs.length - 1} + hasNext={selectedLogIndex < sortedLogs.length - 1} hasPrev={selectedLogIndex > 0} /> ), @@ -732,7 +768,7 @@ export default function Logs() { handleNavigateNext, handleNavigatePrev, selectedLogIndex, - logs.length, + sortedLogs.length, ] ) @@ -978,6 +1014,21 @@ export default function Logs() { [appliedFilters, textSearch, removeBadge, handleFiltersChange] ) + const sortConfig = useMemo( + () => ({ + options: [ + { id: 'date', label: 'Date' }, + { id: 'duration', label: 'Duration' }, + { id: 'cost', label: 'Cost' }, + { id: 'status', label: 'Status' }, + ], + active: activeSort, + onSort: (column, direction) => setActiveSort({ column, direction }), + onClear: () => setActiveSort(null), + }), + [activeSort] + ) + const searchConfig = useMemo( () => ({ value: currentInput, @@ -1065,6 +1116,7 @@ export default function Logs() { } From ffe6806c6cfb0e2697fabe54bdcf27928db12b8b Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 19:15:27 -0700 Subject: [PATCH 21/30] improvement(knowledge): upgrade document list filter to combobox style --- .../knowledge/[id]/[documentId]/document.tsx | 132 +++++++++++++----- .../[workspaceId]/knowledge/[id]/base.tsx | 95 ++++++++----- 2 files changed, 157 insertions(+), 70 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx index 2bb41401cfc..22bdab82904 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx @@ -15,7 +15,6 @@ import { Trash, } from '@/components/emcn' import { SearchHighlight } from '@/components/ui/search-highlight' -import { cn } from '@/lib/core/utils/cn' import type { ChunkData } from '@/lib/knowledge/types' import { formatTokenCount } from '@/lib/tokenization' import type { @@ -152,7 +151,14 @@ export function Document({ const [searchQuery, setSearchQuery] = useState('') const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('') - const [enabledFilter, setEnabledFilter] = useState<'all' | 'enabled' | 'disabled'>('all') + const [enabledFilter, setEnabledFilter] = useState([]) + const [activeSort, setActiveSort] = useState<{ + column: string + direction: 'asc' | 'desc' + } | null>(null) + + const enabledFilterParam = + enabledFilter.length === 1 ? (enabledFilter[0] as 'enabled' | 'disabled') : 'all' const { chunks: initialChunks, @@ -165,7 +171,7 @@ export function Document({ refreshChunks: initialRefreshChunks, updateChunk: initialUpdateChunk, isFetching: isFetchingChunks, - } = useDocumentChunks(knowledgeBaseId, documentId, currentPageFromURL, '', enabledFilter) + } = useDocumentChunks(knowledgeBaseId, documentId, currentPageFromURL, '', enabledFilterParam) const { data: searchResults = [], error: searchQueryError } = useDocumentChunkSearchQuery( { @@ -229,7 +235,28 @@ export function Document({ searchStartIndex + SEARCH_PAGE_SIZE ) - const displayChunks = showingSearch ? paginatedSearchResults : initialChunks + const rawDisplayChunks = showingSearch ? paginatedSearchResults : initialChunks + + const displayChunks = useMemo(() => { + if (!activeSort || !rawDisplayChunks) return rawDisplayChunks ?? [] + const { column, direction } = activeSort + return [...rawDisplayChunks].sort((a, b) => { + let cmp = 0 + switch (column) { + case 'index': + cmp = a.chunkIndex - b.chunkIndex + break + case 'tokens': + cmp = (a.tokenCount ?? 0) - (b.tokenCount ?? 0) + break + case 'status': + cmp = (a.enabled ? 1 : 0) - (b.enabled ? 1 : 0) + break + } + return direction === 'asc' ? cmp : -cmp + }) + }, [rawDisplayChunks, activeSort]) + const currentPage = showingSearch ? searchCurrentPage : initialPage const totalPages = showingSearch ? searchTotalPages : initialTotalPages const hasNextPage = showingSearch ? searchCurrentPage < searchTotalPages : initialHasNextPage @@ -562,46 +589,62 @@ export function Document({ } : undefined + const enabledDisplayLabel = useMemo(() => { + if (enabledFilter.length === 0) return 'All' + if (enabledFilter.length === 1) return enabledFilter[0] === 'enabled' ? 'Enabled' : 'Disabled' + return `${enabledFilter.length} selected` + }, [enabledFilter]) + const filterContent = ( -
-
+
+
Status + { + setEnabledFilter(values) + setSelectedChunks(new Set()) + void goToPage(1) + }} + overlayContent={ + {enabledDisplayLabel} + } + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + />
-
- {(['all', 'enabled', 'disabled'] as const).map((value) => ( - - ))} -
+ {enabledFilter.length > 0 && ( + + )}
) const filterTags: FilterTag[] = [ - ...(enabledFilter !== 'all' - ? [ - { - label: `Status: ${enabledFilter === 'enabled' ? 'Enabled' : 'Disabled'}`, - onRemove: () => { - setEnabledFilter('all') - setSelectedChunks(new Set()) - void goToPage(1) - }, - }, - ] - : []), + ...enabledFilter.map((value) => ({ + label: `Status: ${value === 'enabled' ? 'Enabled' : 'Disabled'}`, + onRemove: () => { + setEnabledFilter(enabledFilter.filter((v) => v !== value)) + setSelectedChunks(new Set()) + void goToPage(1) + }, + })), ] const handleChunkClick = useCallback((rowId: string) => { @@ -814,6 +857,20 @@ export function Document({ } : undefined + const sortConfig: SortConfig = useMemo( + () => ({ + options: [ + { id: 'index', label: 'Index' }, + { id: 'tokens', label: 'Tokens' }, + { id: 'status', label: 'Status' }, + ], + active: activeSort, + onSort: (column, direction) => setActiveSort({ column, direction }), + onClear: () => setActiveSort(null), + }), + [activeSort] + ) + const chunkRows: ResourceRow[] = useMemo(() => { if (!isCompleted) { return [ @@ -1100,6 +1157,7 @@ export function Document({ emptyMessage={emptyMessage} filter={combinedError ? undefined : filterContent} filterTags={combinedError ? undefined : filterTags} + sort={combinedError ? undefined : sortConfig} /> ('all') + const [enabledFilter, setEnabledFilter] = useState([]) const [tagFilterEntries, setTagFilterEntries] = useState< { id: string @@ -235,6 +235,17 @@ export function KnowledgeBase({ [tagFilterEntries] ) + const enabledFilterParam = useMemo<'all' | 'enabled' | 'disabled'>(() => { + if (enabledFilter.length === 1) return enabledFilter[0] as 'enabled' | 'disabled' + return 'all' + }, [enabledFilter]) + + const enabledDisplayLabel = useMemo(() => { + if (enabledFilter.length === 0) return 'All' + if (enabledFilter.length === 1) return enabledFilter[0] === 'enabled' ? 'Enabled' : 'Disabled' + return '2 selected' + }, [enabledFilter]) + const handleSearchChange = useCallback((newQuery: string) => { setSearchQuery(newQuery) setCurrentPage(1) @@ -301,7 +312,7 @@ export function KnowledgeBase({ if (hasSyncingConnectorsRef.current) return 5000 return false }, - enabledFilter, + enabledFilter: enabledFilterParam, tagFilters: activeTagFilters.length > 0 ? activeTagFilters : undefined, }) @@ -571,7 +582,7 @@ export function KnowledgeBase({ knowledgeBaseId: id, operation: 'enable', selectAll: true, - enabledFilter, + enabledFilter: enabledFilterParam, }, { onSuccess: (result) => { @@ -618,7 +629,7 @@ export function KnowledgeBase({ knowledgeBaseId: id, operation: 'disable', selectAll: true, - enabledFilter, + enabledFilter: enabledFilterParam, }, { onSuccess: (result) => { @@ -667,7 +678,7 @@ export function KnowledgeBase({ knowledgeBaseId: id, operation: 'delete', selectAll: true, - enabledFilter, + enabledFilter: enabledFilterParam, }, { onSuccess: (result) => { @@ -707,12 +718,12 @@ export function KnowledgeBase({ const selectedDocumentsList = documents.filter((doc) => selectedDocuments.has(doc.id)) const enabledCount = isSelectAllMode - ? enabledFilter === 'disabled' + ? enabledFilterParam === 'disabled' ? 0 : pagination.total : selectedDocumentsList.filter((doc) => doc.enabled).length const disabledCount = isSelectAllMode - ? enabledFilter === 'enabled' + ? enabledFilterParam === 'enabled' ? 0 : pagination.total : selectedDocumentsList.filter((doc) => !doc.enabled).length @@ -813,30 +824,45 @@ export function KnowledgeBase({ } const filterContent = ( -
-
+
+
Status + { + setEnabledFilter(values) + setCurrentPage(1) + setSelectedDocuments(new Set()) + setIsSelectAllMode(false) + }} + overlayContent={ + {enabledDisplayLabel} + } + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + />
-
- {(['all', 'enabled', 'disabled'] as const).map((value) => ( - - ))} -
+ {enabledFilter.length > 0 && ( + + )} 0 ? [ { - label: `Status: ${enabledFilter === 'enabled' ? 'Enabled' : 'Disabled'}`, + label: + enabledFilter.length === 1 + ? `Status: ${enabledFilter[0] === 'enabled' ? 'Enabled' : 'Disabled'}` + : 'Status: 2 selected', onRemove: () => { - setEnabledFilter('all') + setEnabledFilter([]) setCurrentPage(1) setSelectedDocuments(new Set()) setIsSelectAllMode(false) @@ -1019,7 +1048,7 @@ export function KnowledgeBase({ const emptyMessage = searchQuery ? 'No documents found' - : enabledFilter !== 'all' || activeTagFilters.length > 0 + : enabledFilter.length > 0 || activeTagFilters.length > 0 ? 'Nothing matches your filter' : undefined From 483c35cebffc9c1d5040955a485a0ed68af4406f Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 19:26:56 -0700 Subject: [PATCH 22/30] fix(resources): fix missing imports, memoization, and stale refs across resource pages --- .../workspace/[workspaceId]/files/files.tsx | 3 +- .../knowledge/[id]/[documentId]/document.tsx | 30 +++--- .../[workspaceId]/knowledge/[id]/base.tsx | 94 ++++++++++--------- .../app/workspace/[workspaceId]/logs/logs.tsx | 11 ++- .../recently-deleted/recently-deleted.tsx | 75 +++++---------- .../workspace/[workspaceId]/tables/tables.tsx | 19 +++- 6 files changed, 119 insertions(+), 113 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index f02614cac43..24fc32dcc20 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -6,6 +6,7 @@ import { useParams, useRouter } from 'next/navigation' import { Button, Columns2, + Combobox, type ComboboxOption, Download, DropdownMenu, @@ -388,7 +389,7 @@ export function Files() { } } }, - [workspaceId] + [workspaceId, uploadFile] ) const handleDownload = useCallback(async (file: WorkspaceFileRecord) => { diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx index 22bdab82904..f3af995ff3b 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx @@ -7,6 +7,7 @@ import { useParams, useRouter, useSearchParams } from 'next/navigation' import { Badge, Button, + Combobox, Modal, ModalBody, ModalContent, @@ -26,6 +27,7 @@ import type { ResourceRow, SearchConfig, SelectableConfig, + SortConfig, } from '@/app/workspace/[workspaceId]/components' import { Resource, ResourceHeader } from '@/app/workspace/[workspaceId]/components' import { @@ -157,8 +159,10 @@ export function Document({ direction: 'asc' | 'desc' } | null>(null) - const enabledFilterParam = - enabledFilter.length === 1 ? (enabledFilter[0] as 'enabled' | 'disabled') : 'all' + const enabledFilterParam = useMemo( + () => (enabledFilter.length === 1 ? (enabledFilter[0] as 'enabled' | 'disabled') : 'all'), + [enabledFilter] + ) const { chunks: initialChunks, @@ -636,16 +640,18 @@ export function Document({
) - const filterTags: FilterTag[] = [ - ...enabledFilter.map((value) => ({ - label: `Status: ${value === 'enabled' ? 'Enabled' : 'Disabled'}`, - onRemove: () => { - setEnabledFilter(enabledFilter.filter((v) => v !== value)) - setSelectedChunks(new Set()) - void goToPage(1) - }, - })), - ] + const filterTags: FilterTag[] = useMemo( + () => + enabledFilter.map((value) => ({ + label: `Status: ${value === 'enabled' ? 'Enabled' : 'Disabled'}`, + onRemove: () => { + setEnabledFilter((prev) => prev.filter((v) => v !== value)) + setSelectedChunks(new Set()) + void goToPage(1) + }, + })), + [enabledFilter, goToPage] + ) const handleChunkClick = useCallback((rowId: string) => { setSelectedChunkId(rowId) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index 9ab08270dd8..20c35f8bcb2 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -806,22 +806,25 @@ export function KnowledgeBase({ : []), ] - const sortConfig: SortConfig = { - options: [ - { id: 'filename', label: 'Name' }, - { id: 'fileSize', label: 'Size' }, - { id: 'tokenCount', label: 'Tokens' }, - { id: 'chunkCount', label: 'Chunks' }, - { id: 'uploadedAt', label: 'Uploaded' }, - { id: 'enabled', label: 'Status' }, - ], - active: { column: sortBy, direction: sortOrder }, - onSort: (column, direction) => { - setSortBy(column as DocumentSortField) - setSortOrder(direction) - setCurrentPage(1) - }, - } + const sortConfig: SortConfig = useMemo( + () => ({ + options: [ + { id: 'filename', label: 'Name' }, + { id: 'fileSize', label: 'Size' }, + { id: 'tokenCount', label: 'Tokens' }, + { id: 'chunkCount', label: 'Chunks' }, + { id: 'uploadedAt', label: 'Uploaded' }, + { id: 'enabled', label: 'Status' }, + ], + active: { column: sortBy, direction: sortOrder }, + onSort: (column, direction) => { + setSortBy(column as DocumentSortField) + setSortOrder(direction) + setCurrentPage(1) + }, + }), + [sortBy, sortOrder] + ) const filterContent = (
@@ -897,36 +900,39 @@ export function KnowledgeBase({ ) : null - const filterTags: FilterTag[] = [ - ...(enabledFilter.length > 0 - ? [ - { - label: - enabledFilter.length === 1 - ? `Status: ${enabledFilter[0] === 'enabled' ? 'Enabled' : 'Disabled'}` - : 'Status: 2 selected', - onRemove: () => { - setEnabledFilter([]) - setCurrentPage(1) - setSelectedDocuments(new Set()) - setIsSelectAllMode(false) + const filterTags: FilterTag[] = useMemo( + () => [ + ...(enabledFilter.length > 0 + ? [ + { + label: + enabledFilter.length === 1 + ? `Status: ${enabledFilter[0] === 'enabled' ? 'Enabled' : 'Disabled'}` + : 'Status: 2 selected', + onRemove: () => { + setEnabledFilter([]) + setCurrentPage(1) + setSelectedDocuments(new Set()) + setIsSelectAllMode(false) + }, }, + ] + : []), + ...tagFilterEntries + .filter((f) => f.tagSlot && f.value.trim()) + .map((f) => ({ + label: `${f.tagName}: ${f.value}`, + onRemove: () => { + const updated = tagFilterEntries.filter((_, idx) => idx !== tagFilterEntries.indexOf(f)) + setTagFilterEntries(updated) + setCurrentPage(1) + setSelectedDocuments(new Set()) + setIsSelectAllMode(false) }, - ] - : []), - ...tagFilterEntries - .filter((f) => f.tagSlot && f.value.trim()) - .map((f) => ({ - label: `${f.tagName}: ${f.value}`, - onRemove: () => { - const updated = tagFilterEntries.filter((_, idx) => idx !== tagFilterEntries.indexOf(f)) - setTagFilterEntries(updated) - setCurrentPage(1) - setSelectedDocuments(new Set()) - setIsSelectAllMode(false) - }, - })), - ] + })), + ], + [enabledFilter, tagFilterEntries] + ) const selectableConfig: SelectableConfig = { selectedIds: selectedDocuments, diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index fc96f4ddb11..adfccbfdd14 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -39,6 +39,7 @@ import type { ResourceColumn, ResourceRow, SearchConfig, + SortConfig, } from '@/app/workspace/[workspaceId]/components' import { ResourceHeader, @@ -479,12 +480,12 @@ export default function Logs() { const handleLogContextMenu = useCallback( (e: React.MouseEvent, rowId: string) => { e.preventDefault() - const log = logs.find((l) => l.id === rowId) ?? null + const log = sortedLogs.find((l) => l.id === rowId) ?? null setContextMenuPosition({ x: e.clientX, y: e.clientY }) setContextMenuLog(log) setContextMenuOpen(true) }, - [logs] + [sortedLogs] ) const handleCopyExecutionId = useCallback(() => { @@ -1072,7 +1073,7 @@ export default function Logs() { label: 'Export', icon: Download, onClick: handleExport, - disabled: !userPermissions.canEdit || isExporting || logs.length === 0, + disabled: !userPermissions.canEdit || isExporting || sortedLogs.length === 0, }, { label: 'Notifications', @@ -1105,7 +1106,7 @@ export default function Logs() { handleExport, userPermissions.canEdit, isExporting, - logs.length, + sortedLogs.length, handleOpenNotificationSettings, ] ) @@ -1387,7 +1388,7 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr }, [resetFilters, onSearchQueryChange]) return ( -
+
Status r.name.toLowerCase().includes(normalized)) } - const col = activeSort?.column ?? 'deleted' - const dir = activeSort?.direction ?? 'desc' + const col = (activeSort ?? DEFAULT_SORT).column + const dir = (activeSort ?? DEFAULT_SORT).direction return [...items].sort((a, b) => { let cmp = 0 switch (col) { @@ -234,6 +226,7 @@ export function RecentlyDeleted() { }, [resources, activeTab, searchTerm, activeSort]) const showNoResults = searchTerm.trim() && filtered.length === 0 && resources.length > 0 + const selectedSort = activeSort ?? DEFAULT_SORT function handleRestore(resource: DeletedResource) { setRestoringIds((prev) => new Set(prev).add(resource.id)) @@ -285,43 +278,27 @@ export function RecentlyDeleted() { className='h-auto flex-1 border-0 bg-transparent p-0 font-base leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' />
- - - - - - {SORT_OPTIONS.map((option) => { - const isActive = - (activeSort?.column ?? 'deleted') === option.column && - (activeSort?.direction ?? 'desc') === option.direction - return ( - - setActiveSort({ column: option.column, direction: option.direction }) - } - className={isActive ? 'bg-[var(--bg-selected)]' : ''} - > - {option.label} - +
+ { + const option = SORT_OPTIONS.find( + (sortOption) => `${sortOption.column}:${sortOption.direction}` === value ) - })} - - + if (option) { + setActiveSort({ column: option.column, direction: option.direction }) + } + }} + options={SORT_OPTIONS.map((option) => ({ + label: option.label, + value: `${option.column}:${option.direction}`, + }))} + className='h-[30px] rounded-lg border-[var(--border)] bg-transparent px-2.5 text-small dark:bg-[var(--surface-4)]' + /> +
setActiveTab(v as ResourceType)}> diff --git a/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx b/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx index 7d7604c4ada..95d46382005 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx @@ -137,10 +137,24 @@ export function Tables() { case 'updated': cmp = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime() break + case 'owner': { + const aName = members?.find((m) => m.userId === a.createdBy)?.name ?? '' + const bName = members?.find((m) => m.userId === b.createdBy)?.name ?? '' + cmp = aName.localeCompare(bName) + break + } } return dir === 'asc' ? cmp : -cmp }) - }, [tables, debouncedSearchTerm, rowCountFilter, ownerFilter, columnTypeFilter, activeSort]) + }, [ + tables, + debouncedSearchTerm, + rowCountFilter, + ownerFilter, + columnTypeFilter, + activeSort, + members, + ]) const rows: ResourceRow[] = useMemo( () => @@ -184,6 +198,7 @@ export function Tables() { { id: 'columns', label: 'Columns' }, { id: 'rows', label: 'Rows' }, { id: 'created', label: 'Created' }, + { id: 'owner', label: 'Owner' }, { id: 'updated', label: 'Last Updated' }, ], active: activeSort, @@ -466,7 +481,7 @@ export function Tables() { } } }, - [workspaceId, router] + [workspaceId, router, uploadCsv] ) const handleListUploadCsv = useCallback(() => { From c8672f7fb61d7a4d54606164f797eeb9f8e4cca5 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 19:32:00 -0700 Subject: [PATCH 23/30] improvement(tables): remove column type filter --- .../workspace/[workspaceId]/tables/tables.tsx | 74 +------------------ 1 file changed, 3 insertions(+), 71 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx b/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx index 95d46382005..3d3463d30b2 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx @@ -76,7 +76,6 @@ export function Tables() { } | null>(null) const [rowCountFilter, setRowCountFilter] = useState([]) const [ownerFilter, setOwnerFilter] = useState([]) - const [columnTypeFilter, setColumnTypeFilter] = useState([]) const [uploading, setUploading] = useState(false) const [uploadProgress, setUploadProgress] = useState({ completed: 0, total: 0 }) const csvInputRef = useRef(null) @@ -111,12 +110,6 @@ export function Tables() { if (ownerFilter.length > 0) { result = result.filter((t) => ownerFilter.includes(t.createdBy)) } - if (columnTypeFilter.length > 0) { - result = result.filter((t) => - t.schema.columns.some((col) => columnTypeFilter.includes(col.type)) - ) - } - const col = activeSort?.column ?? 'created' const dir = activeSort?.direction ?? 'desc' return [...result].sort((a, b) => { @@ -146,15 +139,7 @@ export function Tables() { } return dir === 'asc' ? cmp : -cmp }) - }, [ - tables, - debouncedSearchTerm, - rowCountFilter, - ownerFilter, - columnTypeFilter, - activeSort, - members, - ]) + }, [tables, debouncedSearchTerm, rowCountFilter, ownerFilter, activeSort, members]) const rows: ResourceRow[] = useMemo( () => @@ -221,21 +206,6 @@ export function Tables() { return `${rowCountFilter.length} selected` }, [rowCountFilter]) - const columnTypeDisplayLabel = useMemo(() => { - if (columnTypeFilter.length === 0) return 'All' - if (columnTypeFilter.length === 1) { - const labels: Record = { - string: 'Text', - number: 'Number', - boolean: 'Boolean', - date: 'Date', - json: 'JSON', - } - return labels[columnTypeFilter[0]] ?? columnTypeFilter[0] - } - return `${columnTypeFilter.length} selected` - }, [columnTypeFilter]) - const ownerDisplayLabel = useMemo(() => { if (ownerFilter.length === 0) return 'All' if (ownerFilter.length === 1) @@ -264,8 +234,7 @@ export function Tables() { [members] ) - const hasActiveFilters = - rowCountFilter.length > 0 || columnTypeFilter.length > 0 || ownerFilter.length > 0 + const hasActiveFilters = rowCountFilter.length > 0 || ownerFilter.length > 0 const filterContent = (
@@ -289,28 +258,6 @@ export function Tables() { className='h-[32px] w-full rounded-md' />
-
- Column Types - {columnTypeDisplayLabel} - } - showAllOption - allOptionLabel='All' - size='sm' - className='h-[32px] w-full rounded-md' - /> -
{memberOptions.length > 0 && (
Owner @@ -336,7 +283,6 @@ export function Tables() { type='button' onClick={() => { setRowCountFilter([]) - setColumnTypeFilter([]) setOwnerFilter([]) }} className='flex h-[32px] w-full items-center justify-center rounded-md text-[var(--text-secondary)] text-caption transition-colors hover-hover:bg-[var(--surface-active)]' @@ -357,20 +303,6 @@ export function Tables() { : `Rows: ${rowCountFilter.length} selected` tags.push({ label, onRemove: () => setRowCountFilter([]) }) } - if (columnTypeFilter.length > 0) { - const typeLabels: Record = { - string: 'Text', - number: 'Number', - boolean: 'Boolean', - date: 'Date', - json: 'JSON', - } - const label = - columnTypeFilter.length === 1 - ? `Type: ${typeLabels[columnTypeFilter[0]]}` - : `Types: ${columnTypeFilter.length} selected` - tags.push({ label, onRemove: () => setColumnTypeFilter([]) }) - } if (ownerFilter.length > 0) { const label = ownerFilter.length === 1 @@ -379,7 +311,7 @@ export function Tables() { tags.push({ label, onRemove: () => setOwnerFilter([]) }) } return tags - }, [rowCountFilter, columnTypeFilter, ownerFilter, members]) + }, [rowCountFilter, ownerFilter, members]) const handleContentContextMenu = useCallback( (e: React.MouseEvent) => { From 71794bbee21adf8497e98a02b8cf983c64adf6fc Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 19:35:48 -0700 Subject: [PATCH 24/30] fix(resources): fix filter/sort correctness issues from audit --- apps/sim/app/workspace/[workspaceId]/files/files.tsx | 3 --- apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx | 2 +- .../[workspaceId]/scheduled-tasks/scheduled-tasks.tsx | 2 +- apps/sim/app/workspace/[workspaceId]/tables/tables.tsx | 4 ++-- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 24fc32dcc20..31f4a38edd1 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -94,7 +94,6 @@ const COLUMNS: ResourceColumn[] = [ { id: 'type', header: 'Type' }, { id: 'created', header: 'Created' }, { id: 'owner', header: 'Owner' }, - { id: 'updated', header: 'Last Updated' }, ] const MIME_TYPE_LABELS: Record = { @@ -255,7 +254,6 @@ export function Files() { cmp = formatFileType(a.type, a.name).localeCompare(formatFileType(b.type, b.name)) break case 'created': - case 'updated': cmp = new Date(a.uploadedAt).getTime() - new Date(b.uploadedAt).getTime() break } @@ -297,7 +295,6 @@ export function Files() { }, created: timeCell(file.uploadedAt), owner: ownerCell(file.uploadedBy, members), - updated: timeCell(file.uploadedAt), }, } nextCache.set(file.id, { row, file, members }) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index 20c35f8bcb2..8092757c64c 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -923,7 +923,7 @@ export function KnowledgeBase({ .map((f) => ({ label: `${f.tagName}: ${f.value}`, onRemove: () => { - const updated = tagFilterEntries.filter((_, idx) => idx !== tagFilterEntries.indexOf(f)) + const updated = tagFilterEntries.filter((e) => e.id !== f.id) setTagFilterEntries(updated) setCurrentPage(1) setSelectedDocuments(new Set()) diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx index e7fb50c31ee..bad3369a8c1 100644 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx +++ b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx @@ -127,7 +127,7 @@ export function ScheduledTasks() { }) } - if (healthFilter.length > 0 && healthFilter.includes('has-failures')) { + if (healthFilter.includes('has-failures')) { result = result.filter((item) => (item.failedCount ?? 0) > 0) } diff --git a/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx b/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx index 3d3463d30b2..cb2a8d9cb50 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx @@ -199,7 +199,7 @@ export function Tables() { const labels: Record = { empty: 'Empty', small: 'Small (1–100)', - large: 'Large (100+)', + large: 'Large (101+)', } return labels[rowCountFilter[0]] ?? rowCountFilter[0] } @@ -244,7 +244,7 @@ export function Tables() { options={[ { value: 'empty', label: 'Empty' }, { value: 'small', label: 'Small (1–100 rows)' }, - { value: 'large', label: 'Large (100+ rows)' }, + { value: 'large', label: 'Large (101+ rows)' }, ]} multiSelect multiSelectValues={rowCountFilter} From aeeec6b3bc8dde7462195b1fbef1a275e3f0bdb8 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 20:17:40 -0700 Subject: [PATCH 25/30] fix(chunks): add server-side sort to document chunks API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chunk sort was previously done client-side on a single page of server-paginated data, which only reordered the current page. Now sort params (sortBy, sortOrder) flow through the full stack: types → service → API route → query hook → useDocumentChunks → document.tsx. --- .../documents/[documentId]/chunks/route.ts | 4 + .../home/components/user-input/user-input.tsx | 10 +- .../knowledge/[id]/[documentId]/document.tsx | 48 +-- .../[workspaceId]/knowledge/[id]/base.tsx | 2 +- .../collapsed-sidebar-menu.tsx | 3 - .../w/components/sidebar/sidebar.tsx | 286 +++++++++--------- apps/sim/hooks/kb/use-knowledge.ts | 14 +- apps/sim/hooks/queries/kb/knowledge.ts | 8 + apps/sim/lib/knowledge/chunks/service.ts | 23 +- apps/sim/lib/knowledge/chunks/types.ts | 2 + 10 files changed, 213 insertions(+), 187 deletions(-) diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts index 762f9be66cf..4eaa3353f1d 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts @@ -15,6 +15,8 @@ const GetChunksQuerySchema = z.object({ enabled: z.enum(['true', 'false', 'all']).optional().default('all'), limit: z.coerce.number().min(1).max(100).optional().default(50), offset: z.coerce.number().min(0).optional().default(0), + sortBy: z.enum(['chunkIndex', 'tokenCount', 'enabled']).optional().default('chunkIndex'), + sortOrder: z.enum(['asc', 'desc']).optional().default('asc'), }) const CreateChunkSchema = z.object({ @@ -88,6 +90,8 @@ export async function GET( enabled: searchParams.get('enabled') || undefined, limit: searchParams.get('limit') || undefined, offset: searchParams.get('offset') || undefined, + sortBy: searchParams.get('sortBy') || undefined, + sortOrder: searchParams.get('sortOrder') || undefined, }) const result = await queryChunks(documentId, queryParams, requestId) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx index b830123d1a0..2735afc993e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx @@ -571,19 +571,19 @@ export function UserInput({ const items = e.clipboardData?.items if (!items) return - const imageFiles: File[] = [] + const pastedFiles: File[] = [] for (const item of Array.from(items)) { - if (item.kind === 'file' && item.type.startsWith('image/')) { + if (item.kind === 'file') { const file = item.getAsFile() - if (file) imageFiles.push(file) + if (file) pastedFiles.push(file) } } - if (imageFiles.length === 0) return + if (pastedFiles.length === 0) return e.preventDefault() const dt = new DataTransfer() - for (const file of imageFiles) { + for (const file of pastedFiles) { dt.items.add(file) } filesRef.current.processFiles(dt.files) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx index f3af995ff3b..a31e4a721f3 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx @@ -175,7 +175,21 @@ export function Document({ refreshChunks: initialRefreshChunks, updateChunk: initialUpdateChunk, isFetching: isFetchingChunks, - } = useDocumentChunks(knowledgeBaseId, documentId, currentPageFromURL, '', enabledFilterParam) + } = useDocumentChunks( + knowledgeBaseId, + documentId, + currentPageFromURL, + '', + enabledFilterParam, + activeSort?.column === 'tokens' + ? 'tokenCount' + : activeSort?.column === 'status' + ? 'enabled' + : activeSort + ? 'chunkIndex' + : undefined, + activeSort?.direction + ) const { data: searchResults = [], error: searchQueryError } = useDocumentChunkSearchQuery( { @@ -241,25 +255,7 @@ export function Document({ const rawDisplayChunks = showingSearch ? paginatedSearchResults : initialChunks - const displayChunks = useMemo(() => { - if (!activeSort || !rawDisplayChunks) return rawDisplayChunks ?? [] - const { column, direction } = activeSort - return [...rawDisplayChunks].sort((a, b) => { - let cmp = 0 - switch (column) { - case 'index': - cmp = a.chunkIndex - b.chunkIndex - break - case 'tokens': - cmp = (a.tokenCount ?? 0) - (b.tokenCount ?? 0) - break - case 'status': - cmp = (a.enabled ? 1 : 0) - (b.enabled ? 1 : 0) - break - } - return direction === 'asc' ? cmp : -cmp - }) - }, [rawDisplayChunks, activeSort]) + const displayChunks = rawDisplayChunks ?? [] const currentPage = showingSearch ? searchCurrentPage : initialPage const totalPages = showingSearch ? searchTotalPages : initialTotalPages @@ -871,10 +867,16 @@ export function Document({ { id: 'status', label: 'Status' }, ], active: activeSort, - onSort: (column, direction) => setActiveSort({ column, direction }), - onClear: () => setActiveSort(null), + onSort: (column, direction) => { + setActiveSort({ column, direction }) + void goToPage(1) + }, + onClear: () => { + setActiveSort(null) + void goToPage(1) + }, }), - [activeSort] + [activeSort, goToPage] ) const chunkRows: ResourceRow[] = useMemo(() => { diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index 8092757c64c..1a5676cbea9 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -957,7 +957,7 @@ export function KnowledgeBase({ content: ( -
{getStatusBadge(doc)}
+
{getStatusBadge(doc)}
{doc.processingError} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx index 79aeab6f1ca..7a64c8e0392 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx @@ -245,9 +245,6 @@ export function CollapsedTaskFlyoutItem({ title={task.name} isActive={!!task.isActive} isUnread={!!task.isUnread} - statusIndicatorClassName={ - !(isCurrentRoute || isMenuOpen) ? 'group-hover:hidden' : undefined - } /> {showActions && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index df8f68cff4d..836029439c5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -100,6 +100,28 @@ import { useWorkflowRegistry } from '@/stores/workflows/registry/store' const logger = createLogger('Sidebar') +export function SidebarTooltip({ + children, + label, + enabled, + side = 'right', +}: { + children: React.ReactElement + label: string + enabled: boolean + side?: 'right' | 'bottom' +}) { + if (!enabled) return children + return ( + + {children} + +

{label}

+
+
+ ) +} + function SidebarItemSkeleton() { return (
@@ -135,71 +157,61 @@ const SidebarTaskItem = memo(function SidebarTaskItem({ onMoreClick: (e: React.MouseEvent, taskId: string) => void }) { return ( - - - { - if (task.id === 'new') return - if (e.shiftKey || e.metaKey || e.ctrlKey) { - e.preventDefault() - onMultiSelectClick(task.id, e.shiftKey, e.metaKey || e.ctrlKey) - } else { - useFolderStore.setState({ - selectedTasks: new Set(), - lastSelectedTaskId: task.id, - }) - } - }} - onContextMenu={task.id !== 'new' ? (e) => onContextMenu(e, task.id) : undefined} - > - -
- {task.name} -
- {task.id !== 'new' && ( -
- {isActive && !isCurrentRoute && ( - - )} - {isActive && !isCurrentRoute && ( - - )} - {!isActive && isUnread && !isCurrentRoute && ( - + + { + if (task.id === 'new') return + if (e.shiftKey || e.metaKey || e.ctrlKey) { + e.preventDefault() + onMultiSelectClick(task.id, e.shiftKey, e.metaKey || e.ctrlKey) + } else { + useFolderStore.setState({ + selectedTasks: new Set(), + lastSelectedTaskId: task.id, + }) + } + }} + onContextMenu={task.id !== 'new' ? (e) => onContextMenu(e, task.id) : undefined} + > + +
{task.name}
+ {task.id !== 'new' && ( +
+ {isActive && !isCurrentRoute && ( + + )} + {isActive && !isCurrentRoute && ( + + )} + {!isActive && isUnread && !isCurrentRoute && ( + + )} + -
- )} - - - {showCollapsedTooltips && ( - -

{task.name}

-
- )} - + > + + +
+ )} + + ) }) @@ -265,15 +277,12 @@ const SidebarNavItem = memo(function SidebarNavItem({ ) : null + if (!element) return null + return ( - - {element} - {showCollapsedTooltips && ( - -

{item.label}

-
- )} -
+ + {element} + ) }) @@ -317,6 +326,7 @@ export const Sidebar = memo(function Sidebar() { const setSidebarWidth = useSidebarStore((state) => state.setSidebarWidth) const isCollapsed = useSidebarStore((state) => state.isCollapsed) const toggleCollapsed = useSidebarStore((state) => state.toggleCollapsed) + const _hasHydrated = useSidebarStore((state) => state._hasHydrated) const isOnWorkflowPage = !!workflowId const isCollapsedRef = useRef(isCollapsed) @@ -330,10 +340,13 @@ export const Sidebar = memo(function Sidebar() { const [showCollapsedTooltips, setShowCollapsedTooltips] = useState(isCollapsed) useLayoutEffect(() => { - if (!isCollapsed) { + if (!_hasHydrated) return + if (isCollapsed) { + document.documentElement.setAttribute('data-sidebar-collapsed', '') + } else { document.documentElement.removeAttribute('data-sidebar-collapsed') } - }, [isCollapsed]) + }, [isCollapsed, _hasHydrated]) useEffect(() => { if (isCollapsed) { @@ -1208,63 +1221,45 @@ export const Sidebar = memo(function Sidebar() { {/* Top bar: Logo + Collapse toggle */}
- - - - {brand.logoUrl ? ( - {brand.name} - ) : isCollapsed ? ( - - ) : ( - - )} - {isCollapsed && ( - - )} - - - {showCollapsedTooltips && ( - -

Expand sidebar

-
- )} -
-
- - - - - {!isCollapsed && ( - -

Collapse sidebar

-
- )} -
+ {brand.logoUrl ? ( + {brand.name} + ) : ( + <> + + + + )} + + + +
+ + +
{/* Workspace Header */} @@ -1621,27 +1616,20 @@ export const Sidebar = memo(function Sidebar() { > {/* Help dropdown */} - + - - - + - {showCollapsedTooltips && ( - -

Help

-
- )} -
+ diff --git a/apps/sim/hooks/kb/use-knowledge.ts b/apps/sim/hooks/kb/use-knowledge.ts index 39bfbeb06da..afef640adda 100644 --- a/apps/sim/hooks/kb/use-knowledge.ts +++ b/apps/sim/hooks/kb/use-knowledge.ts @@ -233,7 +233,9 @@ export function useDocumentChunks( documentId: string, page = 1, search = '', - enabledFilter: 'all' | 'enabled' | 'disabled' = 'all' + enabledFilter: 'all' | 'enabled' | 'disabled' = 'all', + sortBy?: 'chunkIndex' | 'tokenCount' | 'enabled', + sortOrder?: 'asc' | 'desc' ) { const queryClient = useQueryClient() @@ -248,6 +250,8 @@ export function useDocumentChunks( offset, search: search || undefined, enabledFilter, + sortBy, + sortOrder, }, { enabled: Boolean(knowledgeBaseId && documentId), @@ -280,11 +284,13 @@ export function useDocumentChunks( offset, search: search || undefined, enabledFilter, + sortBy, + sortOrder, }) await queryClient.invalidateQueries({ queryKey: knowledgeKeys.chunks(knowledgeBaseId, documentId, paramsKey), }) - }, [knowledgeBaseId, documentId, offset, search, enabledFilter, queryClient]) + }, [knowledgeBaseId, documentId, offset, search, enabledFilter, sortBy, sortOrder, queryClient]) const updateChunk = useCallback( (chunkId: string, updates: Partial) => { @@ -295,6 +301,8 @@ export function useDocumentChunks( offset, search: search || undefined, enabledFilter, + sortBy, + sortOrder, }) queryClient.setQueryData( knowledgeKeys.chunks(knowledgeBaseId, documentId, paramsKey), @@ -309,7 +317,7 @@ export function useDocumentChunks( } ) }, - [knowledgeBaseId, documentId, offset, search, enabledFilter, queryClient] + [knowledgeBaseId, documentId, offset, search, enabledFilter, sortBy, sortOrder, queryClient] ) return { diff --git a/apps/sim/hooks/queries/kb/knowledge.ts b/apps/sim/hooks/queries/kb/knowledge.ts index 53b938690d5..455d762ecab 100644 --- a/apps/sim/hooks/queries/kb/knowledge.ts +++ b/apps/sim/hooks/queries/kb/knowledge.ts @@ -181,6 +181,8 @@ export interface KnowledgeChunksParams { enabledFilter?: 'all' | 'enabled' | 'disabled' limit?: number offset?: number + sortBy?: 'chunkIndex' | 'tokenCount' | 'enabled' + sortOrder?: 'asc' | 'desc' } export interface KnowledgeChunksResponse { @@ -196,6 +198,8 @@ export async function fetchKnowledgeChunks( enabledFilter, limit = 50, offset = 0, + sortBy, + sortOrder, }: KnowledgeChunksParams, signal?: AbortSignal ): Promise { @@ -206,6 +210,8 @@ export async function fetchKnowledgeChunks( } if (limit) params.set('limit', limit.toString()) if (offset) params.set('offset', offset.toString()) + if (sortBy && sortBy !== 'chunkIndex') params.set('sortBy', sortBy) + if (sortOrder && sortOrder !== 'asc') params.set('sortOrder', sortOrder) const response = await fetch( `/api/knowledge/${knowledgeBaseId}/documents/${documentId}/chunks${params.toString() ? `?${params.toString()}` : ''}`, @@ -306,6 +312,8 @@ export const serializeChunkParams = (params: KnowledgeChunksParams) => enabledFilter: params.enabledFilter ?? 'all', limit: params.limit ?? 50, offset: params.offset ?? 0, + sortBy: params.sortBy ?? 'chunkIndex', + sortOrder: params.sortOrder ?? 'asc', }) export function useKnowledgeChunksQuery( diff --git a/apps/sim/lib/knowledge/chunks/service.ts b/apps/sim/lib/knowledge/chunks/service.ts index c4aef86d270..ba75ce0cdc0 100644 --- a/apps/sim/lib/knowledge/chunks/service.ts +++ b/apps/sim/lib/knowledge/chunks/service.ts @@ -2,7 +2,7 @@ import { createHash, randomUUID } from 'crypto' import { db } from '@sim/db' import { document, embedding, knowledgeBase } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, asc, eq, ilike, inArray, isNull, sql } from 'drizzle-orm' +import { and, asc, desc, eq, ilike, inArray, isNull, sql } from 'drizzle-orm' import type { BatchOperationResult, ChunkData, @@ -23,7 +23,14 @@ export async function queryChunks( filters: ChunkFilters, requestId: string ): Promise { - const { search, enabled = 'all', limit = 50, offset = 0 } = filters + const { + search, + enabled = 'all', + limit = 50, + offset = 0, + sortBy = 'chunkIndex', + sortOrder = 'asc', + } = filters // Build query conditions const conditions = [eq(embedding.documentId, documentId)] @@ -63,7 +70,17 @@ export async function queryChunks( }) .from(embedding) .where(and(...conditions)) - .orderBy(asc(embedding.chunkIndex)) + .orderBy( + (() => { + const col = + sortBy === 'tokenCount' + ? embedding.tokenCount + : sortBy === 'enabled' + ? embedding.enabled + : embedding.chunkIndex + return sortOrder === 'desc' ? desc(col) : asc(col) + })() + ) .limit(limit) .offset(offset) diff --git a/apps/sim/lib/knowledge/chunks/types.ts b/apps/sim/lib/knowledge/chunks/types.ts index 5c48c450ab6..2532828b2e9 100644 --- a/apps/sim/lib/knowledge/chunks/types.ts +++ b/apps/sim/lib/knowledge/chunks/types.ts @@ -3,6 +3,8 @@ export interface ChunkFilters { enabled?: 'true' | 'false' | 'all' limit?: number offset?: number + sortBy?: 'chunkIndex' | 'tokenCount' | 'enabled' + sortOrder?: 'asc' | 'desc' } export interface ChunkData { From 7b13a1ad64600c008272a45191285e7c91f0316c Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 20:44:54 -0700 Subject: [PATCH 26/30] perf(resources): memoize filterContent JSX across all resource pages Resource is wrapped in React.memo, so an unstable filterContent reference on every parent re-render defeats the memo. Wrap filterContent in useMemo with correct deps in all 6 pages (files, tables, scheduled-tasks, knowledge, base, document). --- .../workspace/[workspaceId]/files/files.tsx | 146 +++++++++-------- .../knowledge/[id]/[documentId]/document.tsx | 79 ++++----- .../[workspaceId]/knowledge/[id]/base.tsx | 93 +++++------ .../[workspaceId]/knowledge/knowledge.tsx | 138 ++++++++-------- .../scheduled-tasks/scheduled-tasks.tsx | 153 ++++++++++-------- .../workspace/[workspaceId]/tables/tables.tsx | 98 ++++++----- 6 files changed, 383 insertions(+), 324 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 31f4a38edd1..39232960ef6 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -871,82 +871,98 @@ export function Files() { const hasActiveFilters = typeFilter.length > 0 || sizeFilter.length > 0 || uploadedByFilter.length > 0 - const filterContent = ( -
-
- File Type - {typeDisplayLabel} - } - showAllOption - allOptionLabel='All' - size='sm' - className='h-[32px] w-full rounded-md' - /> -
-
- Size - 10 MB)' }, - ]} - multiSelect - multiSelectValues={sizeFilter} - onMultiSelectChange={setSizeFilter} - overlayContent={ - {sizeDisplayLabel} - } - showAllOption - allOptionLabel='All' - size='sm' - className='h-[32px] w-full rounded-md' - /> -
- {memberOptions.length > 0 && ( + const filterContent = useMemo( + () => ( +
- Uploaded By + File Type {uploadedByDisplayLabel} + {typeDisplayLabel} } - searchable - searchPlaceholder='Search members...' showAllOption allOptionLabel='All' size='sm' className='h-[32px] w-full rounded-md' />
- )} - {hasActiveFilters && ( - - )} -
+
+ Size + 10 MB)' }, + ]} + multiSelect + multiSelectValues={sizeFilter} + onMultiSelectChange={setSizeFilter} + overlayContent={ + {sizeDisplayLabel} + } + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + /> +
+ {memberOptions.length > 0 && ( +
+ + Uploaded By + + + {uploadedByDisplayLabel} + + } + searchable + searchPlaceholder='Search members...' + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + /> +
+ )} + {hasActiveFilters && ( + + )} +
+ ), + [ + typeFilter, + sizeFilter, + uploadedByFilter, + memberOptions, + typeDisplayLabel, + sizeDisplayLabel, + uploadedByDisplayLabel, + hasActiveFilters, + ] ) const filterTags: FilterTag[] = useMemo(() => { diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx index a31e4a721f3..687babb9f75 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx @@ -595,45 +595,48 @@ export function Document({ return `${enabledFilter.length} selected` }, [enabledFilter]) - const filterContent = ( -
-
- Status - { - setEnabledFilter(values) - setSelectedChunks(new Set()) - void goToPage(1) - }} - overlayContent={ - {enabledDisplayLabel} - } - showAllOption - allOptionLabel='All' - size='sm' - className='h-[32px] w-full rounded-md' - /> + const filterContent = useMemo( + () => ( +
+
+ Status + { + setEnabledFilter(values) + setSelectedChunks(new Set()) + void goToPage(1) + }} + overlayContent={ + {enabledDisplayLabel} + } + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + /> +
+ {enabledFilter.length > 0 && ( + + )}
- {enabledFilter.length > 0 && ( - - )} -
+ ), + [enabledFilter, enabledDisplayLabel, goToPage] ) const filterTags: FilterTag[] = useMemo( diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index 1a5676cbea9..0c879cf1e5d 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -826,57 +826,60 @@ export function KnowledgeBase({ [sortBy, sortOrder] ) - const filterContent = ( -
-
- Status - { - setEnabledFilter(values) + const filterContent = useMemo( + () => ( +
+
+ Status + { + setEnabledFilter(values) + setCurrentPage(1) + setSelectedDocuments(new Set()) + setIsSelectAllMode(false) + }} + overlayContent={ + {enabledDisplayLabel} + } + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + /> +
+ {enabledFilter.length > 0 && ( + + )} + { + setTagFilterEntries(entries) setCurrentPage(1) setSelectedDocuments(new Set()) setIsSelectAllMode(false) }} - overlayContent={ - {enabledDisplayLabel} - } - showAllOption - allOptionLabel='All' - size='sm' - className='h-[32px] w-full rounded-md' />
- {enabledFilter.length > 0 && ( - - )} - { - setTagFilterEntries(entries) - setCurrentPage(1) - setSelectedDocuments(new Set()) - setIsSelectAllMode(false) - }} - /> -
+ ), + [enabledFilter, enabledDisplayLabel, tagDefinitions, tagFilterEntries] ) const connectorBadges = diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx index b1d2ff40bea..3ef8e8f62ce 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx @@ -417,80 +417,92 @@ export function Knowledge() { const hasActiveFilters = connectorFilter.length > 0 || contentFilter.length > 0 || ownerFilter.length > 0 - const filterContent = ( -
-
- Connectors - {connectorDisplayLabel} - } - showAllOption - allOptionLabel='All' - size='sm' - className='h-[32px] w-full rounded-md' - /> -
-
- Content - {contentDisplayLabel} - } - showAllOption - allOptionLabel='All' - size='sm' - className='h-[32px] w-full rounded-md' - /> -
- {memberOptions.length > 0 && ( + const filterContent = useMemo( + () => ( +
- Owner + Connectors {ownerDisplayLabel} + {connectorDisplayLabel} } - searchable - searchPlaceholder='Search members...' showAllOption allOptionLabel='All' size='sm' className='h-[32px] w-full rounded-md' />
- )} - {hasActiveFilters && ( - - )} -
+
+ Content + {contentDisplayLabel} + } + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + /> +
+ {memberOptions.length > 0 && ( +
+ Owner + {ownerDisplayLabel} + } + searchable + searchPlaceholder='Search members...' + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + /> +
+ )} + {hasActiveFilters && ( + + )} +
+ ), + [ + connectorFilter, + contentFilter, + ownerFilter, + memberOptions, + connectorDisplayLabel, + contentDisplayLabel, + ownerDisplayLabel, + hasActiveFilters, + ] ) const filterTags: FilterTag[] = useMemo(() => { diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx index bad3369a8c1..e979a0541a5 100644 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx +++ b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx @@ -271,76 +271,91 @@ export function ScheduledTasks() { const hasActiveFilters = scheduleTypeFilter.length > 0 || statusFilter.length > 0 || healthFilter.length > 0 - const filterContent = ( -
-
- Schedule Type - {scheduleTypeDisplayLabel} - } - showAllOption - allOptionLabel='All' - size='sm' - className='h-[32px] w-full rounded-md' - /> -
-
- Status - {statusDisplayLabel} - } - showAllOption - allOptionLabel='All' - size='sm' - className='h-[32px] w-full rounded-md' - /> -
-
- Health - {healthDisplayLabel} - } - showAllOption - allOptionLabel='All' - size='sm' - className='h-[32px] w-full rounded-md' - /> + const filterContent = useMemo( + () => ( +
+
+ + Schedule Type + + + {scheduleTypeDisplayLabel} + + } + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + /> +
+
+ Status + {statusDisplayLabel} + } + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + /> +
+
+ Health + {healthDisplayLabel} + } + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + /> +
+ {hasActiveFilters && ( + + )}
- {hasActiveFilters && ( - - )} -
+ ), + [ + scheduleTypeFilter, + statusFilter, + healthFilter, + scheduleTypeDisplayLabel, + statusDisplayLabel, + healthDisplayLabel, + hasActiveFilters, + ] ) const filterTags: FilterTag[] = useMemo(() => { diff --git a/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx b/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx index cb2a8d9cb50..95ed0801d9f 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx @@ -236,61 +236,71 @@ export function Tables() { const hasActiveFilters = rowCountFilter.length > 0 || ownerFilter.length > 0 - const filterContent = ( -
-
- Row Count - {rowCountDisplayLabel} - } - showAllOption - allOptionLabel='All' - size='sm' - className='h-[32px] w-full rounded-md' - /> -
- {memberOptions.length > 0 && ( + const filterContent = useMemo( + () => ( +
- Owner + Row Count {ownerDisplayLabel} + {rowCountDisplayLabel} } - searchable - searchPlaceholder='Search members...' showAllOption allOptionLabel='All' size='sm' className='h-[32px] w-full rounded-md' />
- )} - {hasActiveFilters && ( - - )} -
+ {memberOptions.length > 0 && ( +
+ Owner + {ownerDisplayLabel} + } + searchable + searchPlaceholder='Search members...' + showAllOption + allOptionLabel='All' + size='sm' + className='h-[32px] w-full rounded-md' + /> +
+ )} + {hasActiveFilters && ( + + )} +
+ ), + [ + rowCountFilter, + ownerFilter, + memberOptions, + rowCountDisplayLabel, + ownerDisplayLabel, + hasActiveFilters, + ] ) const filterTags: FilterTag[] = useMemo(() => { From 90bd95800237d1d43b0d2b6d06faf3f9b2629a49 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 20:57:48 -0700 Subject: [PATCH 27/30] fix(resources): add missing sort options for all visible columns Every column visible in a resource table should be sortable. Three pages had visible columns with no sort support: - files.tsx: add 'owner' sort (member name lookup) - scheduled-tasks.tsx: add 'schedule' sort (localeCompare on description) - knowledge.tsx: add 'connectors' (count) and 'owner' (member name) sorts Also add 'members' to processedKBs deps in knowledge.tsx since owner sort now reads member names inside the memo. --- apps/sim/app/workspace/[workspaceId]/files/files.tsx | 8 +++++++- .../workspace/[workspaceId]/knowledge/knowledge.tsx | 11 +++++++++++ .../[workspaceId]/scheduled-tasks/scheduled-tasks.tsx | 4 ++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 39232960ef6..70e5e38a851 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -256,10 +256,15 @@ export function Files() { case 'created': cmp = new Date(a.uploadedAt).getTime() - new Date(b.uploadedAt).getTime() break + case 'owner': + cmp = (members?.find((m) => m.userId === a.uploadedBy)?.name ?? '').localeCompare( + members?.find((m) => m.userId === b.uploadedBy)?.name ?? '' + ) + break } return dir === 'asc' ? cmp : -cmp }) - }, [files, debouncedSearchTerm, typeFilter, sizeFilter, uploadedByFilter, activeSort]) + }, [files, debouncedSearchTerm, typeFilter, sizeFilter, uploadedByFilter, activeSort, members]) const rowCacheRef = useRef( new Map() @@ -860,6 +865,7 @@ export function Files() { { id: 'size', label: 'Size' }, { id: 'type', label: 'Type' }, { id: 'created', label: 'Created' }, + { id: 'owner', label: 'Owner' }, ], active: activeSort, onSort: (column, direction) => setActiveSort({ column, direction }), diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx index 3ef8e8f62ce..58bd4ceddae 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx @@ -230,6 +230,14 @@ export function Knowledge() { case 'updated': cmp = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime() break + case 'connectors': + cmp = (a.connectorTypes?.length ?? 0) - (b.connectorTypes?.length ?? 0) + break + case 'owner': + cmp = (members?.find((m) => m.userId === a.userId)?.name ?? '').localeCompare( + members?.find((m) => m.userId === b.userId)?.name ?? '' + ) + break } return dir === 'asc' ? cmp : -cmp }) @@ -240,6 +248,7 @@ export function Knowledge() { contentFilter, ownerFilter, activeSort, + members, ]) const rows: ResourceRow[] = useMemo( @@ -362,8 +371,10 @@ export function Knowledge() { { id: 'name', label: 'Name' }, { id: 'documents', label: 'Documents' }, { id: 'tokens', label: 'Tokens' }, + { id: 'connectors', label: 'Connectors' }, { id: 'created', label: 'Created' }, { id: 'updated', label: 'Last Updated' }, + { id: 'owner', label: 'Owner' }, ], active: activeSort, onSort: (column, direction) => setActiveSort({ column, direction }), diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx index e979a0541a5..2fb6fb3e47c 100644 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx +++ b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx @@ -149,6 +149,9 @@ export function ScheduledTasks() { (a.lastRanAt ? new Date(a.lastRanAt).getTime() : 0) - (b.lastRanAt ? new Date(b.lastRanAt).getTime() : 0) break + case 'schedule': + cmp = getScheduleDescription(a).localeCompare(getScheduleDescription(b)) + break } return dir === 'asc' ? cmp : -cmp }) @@ -240,6 +243,7 @@ export function ScheduledTasks() { () => ({ options: [ { id: 'task', label: 'Task' }, + { id: 'schedule', label: 'Schedule' }, { id: 'nextRun', label: 'Next Run' }, { id: 'lastRun', label: 'Last Run' }, ], From 03187252b958d9c3ea1c5c8b3e4820361c82937d Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 28 Mar 2026 22:45:22 -0700 Subject: [PATCH 28/30] whitelabeling updates, sidebar fixes, files bug --- .../(auth)/components/auth-button-classes.ts | 9 +- apps/sim/app/_styles/globals.css | 6 + .../chat/components/auth/email/email-auth.tsx | 2 +- .../[identifier]/components/error-state.tsx | 6 +- .../[identifier]/components/password-auth.tsx | 3 +- apps/sim/app/form/[identifier]/error.tsx | 6 +- apps/sim/app/form/[identifier]/form.tsx | 7 +- .../sim/app/invite/components/status-card.tsx | 10 +- apps/sim/app/not-found.tsx | 9 +- apps/sim/app/unsubscribe/unsubscribe.tsx | 24 +-- .../components/file-viewer/preview-panel.tsx | 175 +++++++++++------- .../mothership-chat/mothership-chat.tsx | 4 +- .../[tableId]/components/table/table.tsx | 22 +-- .../settings-sidebar/settings-sidebar.tsx | 49 +++-- .../w/components/sidebar/sidebar.tsx | 49 +++-- apps/sim/ee/whitelabeling/inject-theme.ts | 22 ++- apps/sim/hooks/use-auto-scroll.ts | 12 +- 17 files changed, 236 insertions(+), 179 deletions(-) diff --git a/apps/sim/app/(auth)/components/auth-button-classes.ts b/apps/sim/app/(auth)/components/auth-button-classes.ts index 02d1d5e47ed..a55f334ea8e 100644 --- a/apps/sim/app/(auth)/components/auth-button-classes.ts +++ b/apps/sim/app/(auth)/components/auth-button-classes.ts @@ -1,3 +1,6 @@ -/** Shared className for primary auth form submit buttons across all auth pages. */ -export const AUTH_SUBMIT_BTN = - 'inline-flex h-[32px] w-full items-center justify-center gap-2 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-black text-sm transition-colors hover:border-[var(--border-1)] hover:bg-[var(--border-1)] disabled:cursor-not-allowed disabled:opacity-50' as const +/** Shared className for primary auth/status CTA buttons on dark auth surfaces. */ +export const AUTH_PRIMARY_CTA_BASE = + 'inline-flex h-[32px] items-center justify-center gap-2 rounded-[5px] border border-[var(--auth-primary-btn-border)] bg-[var(--auth-primary-btn-bg)] px-2.5 font-[430] font-season text-[var(--auth-primary-btn-text)] text-sm transition-colors hover:border-[var(--auth-primary-btn-hover-border)] hover:bg-[var(--auth-primary-btn-hover-bg)] hover:text-[var(--auth-primary-btn-hover-text)] disabled:cursor-not-allowed disabled:opacity-50' as const + +/** Full-width variant used for primary auth form submit buttons. */ +export const AUTH_SUBMIT_BTN = `${AUTH_PRIMARY_CTA_BASE} w-full` as const diff --git a/apps/sim/app/_styles/globals.css b/apps/sim/app/_styles/globals.css index e781191f6f9..0c80aef072a 100644 --- a/apps/sim/app/_styles/globals.css +++ b/apps/sim/app/_styles/globals.css @@ -15,6 +15,12 @@ --toolbar-triggers-height: 300px; /* TOOLBAR_TRIGGERS_HEIGHT.DEFAULT */ --editor-connections-height: 172px; /* EDITOR_CONNECTIONS_HEIGHT.DEFAULT */ --terminal-height: 206px; /* TERMINAL_HEIGHT.DEFAULT */ + --auth-primary-btn-bg: #ffffff; + --auth-primary-btn-border: #ffffff; + --auth-primary-btn-text: #000000; + --auth-primary-btn-hover-bg: #e0e0e0; + --auth-primary-btn-hover-border: #e0e0e0; + --auth-primary-btn-hover-text: #000000; /* z-index scale for layered UI Popover must be above modal so dropdowns inside modals render correctly */ diff --git a/apps/sim/app/chat/components/auth/email/email-auth.tsx b/apps/sim/app/chat/components/auth/email/email-auth.tsx index 2a9da32b90d..651920a8439 100644 --- a/apps/sim/app/chat/components/auth/email/email-auth.tsx +++ b/apps/sim/app/chat/components/auth/email/email-auth.tsx @@ -293,7 +293,7 @@ export default function EmailAuth({ identifier, onAuthSuccess }: EmailAuthProps) diff --git a/apps/sim/app/form/[identifier]/components/password-auth.tsx b/apps/sim/app/form/[identifier]/components/password-auth.tsx index ecd0ec9d9fb..5f035360a29 100644 --- a/apps/sim/app/form/[identifier]/components/password-auth.tsx +++ b/apps/sim/app/form/[identifier]/components/password-auth.tsx @@ -5,6 +5,7 @@ import { Eye, EyeOff, Loader2 } from 'lucide-react' import { Input, Label } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' import AuthBackground from '@/app/(auth)/components/auth-background' +import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes' import { SupportFooter } from '@/app/(auth)/components/support-footer' import Navbar from '@/app/(home)/components/navbar/navbar' @@ -75,7 +76,7 @@ export function PasswordAuth({ onSubmit, error }: PasswordAuthProps) { diff --git a/apps/sim/app/form/[identifier]/form.tsx b/apps/sim/app/form/[identifier]/form.tsx index 966471d5af3..f46fe4fd644 100644 --- a/apps/sim/app/form/[identifier]/form.tsx +++ b/apps/sim/app/form/[identifier]/form.tsx @@ -5,6 +5,7 @@ import { createLogger } from '@sim/logger' import { Loader2 } from 'lucide-react' import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono' import AuthBackground from '@/app/(auth)/components/auth-background' +import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes' import { SupportFooter } from '@/app/(auth)/components/support-footer' import Navbar from '@/app/(home)/components/navbar/navbar' import { @@ -322,11 +323,7 @@ export default function Form({ identifier }: { identifier: string }) { )} {fields.length > 0 && ( - )} @@ -69,9 +67,9 @@ export function InviteStatusCard({ onClick={action.onClick} disabled={action.disabled || action.loading} className={cn( - 'inline-flex h-[32px] w-full items-center justify-center gap-2 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-black text-sm transition-colors hover:border-[var(--border-1)] hover:bg-[var(--border-1)] disabled:cursor-not-allowed disabled:opacity-50', + `${AUTH_PRIMARY_CTA_BASE} w-full`, index !== 0 && - 'border-[var(--landing-border-strong)] bg-transparent text-[var(--landing-text)] hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-elevated)]' + 'border-[var(--landing-border-strong)] bg-transparent text-[var(--landing-text)] hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-elevated)] hover:text-[var(--landing-text)]' )} > {action.loading ? ( diff --git a/apps/sim/app/not-found.tsx b/apps/sim/app/not-found.tsx index 1877477fbf1..2d1ab6be6fb 100644 --- a/apps/sim/app/not-found.tsx +++ b/apps/sim/app/not-found.tsx @@ -2,6 +2,7 @@ import type { Metadata } from 'next' import Link from 'next/link' import { getNavBlogPosts } from '@/lib/blog/registry' import AuthBackground from '@/app/(auth)/components/auth-background' +import { AUTH_PRIMARY_CTA_BASE } from '@/app/(auth)/components/auth-button-classes' import Navbar from '@/app/(home)/components/navbar/navbar' export const metadata: Metadata = { @@ -9,9 +10,6 @@ export const metadata: Metadata = { robots: { index: false, follow: true }, } -const CTA_BASE = - 'inline-flex items-center h-[32px] rounded-[5px] border px-2.5 font-[430] font-season text-sm' - export default async function NotFound() { const blogPosts = await getNavBlogPosts() return ( @@ -29,10 +27,7 @@ export default async function NotFound() { The page you're looking for doesn't exist or has been moved.

- + Return to Home
diff --git a/apps/sim/app/unsubscribe/unsubscribe.tsx b/apps/sim/app/unsubscribe/unsubscribe.tsx index 3efa7698b3c..85802f5c462 100644 --- a/apps/sim/app/unsubscribe/unsubscribe.tsx +++ b/apps/sim/app/unsubscribe/unsubscribe.tsx @@ -3,6 +3,7 @@ import { Suspense, useEffect, useState } from 'react' import { Loader2 } from 'lucide-react' import { useSearchParams } from 'next/navigation' +import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes' import { InviteLayout } from '@/app/invite/components' interface UnsubscribeData { @@ -143,10 +144,7 @@ function UnsubscribeContent() {
-
@@ -168,10 +166,7 @@ function UnsubscribeContent() {
-
@@ -193,10 +188,7 @@ function UnsubscribeContent() {
-
@@ -222,7 +214,7 @@ function UnsubscribeContent() {
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx index e6c03863f0b..384666e3e07 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx @@ -91,7 +91,9 @@ export function MothershipChat({ }: MothershipChatProps) { const styles = LAYOUT_STYLES[layout] const isStreamActive = isSending || isReconnecting - const { ref: scrollContainerRef, scrollToBottom } = useAutoScroll(isStreamActive) + const { ref: scrollContainerRef, scrollToBottom } = useAutoScroll(isStreamActive, { + scrollOnMount: true, + }) const hasMessages = messages.length > 0 const initialScrollDoneRef = useRef(false) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx index d0189f4b92f..28086ac8869 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx @@ -379,13 +379,11 @@ export function Table({ setColumnWidths(updatedWidths) } const updatedOrder = columnOrderRef.current?.map((n) => (n === columnName ? newName : n)) - if (updatedOrder) { - setColumnOrder(updatedOrder) - updateMetadataRef.current({ - columnWidths: updatedWidths, - columnOrder: updatedOrder, - }) - } + if (updatedOrder) setColumnOrder(updatedOrder) + updateMetadataRef.current({ + columnWidths: updatedWidths, + columnOrder: updatedOrder, + }) updateColumnMutation.mutate({ columnName, updates: { name: newName } }) }, }) @@ -694,6 +692,7 @@ export function Table({ } setDragColumnName(null) setDropTargetColumnName(null) + setDropSide('left') }, []) const handleColumnDragLeave = useCallback(() => { @@ -1352,8 +1351,7 @@ export function Table({ const insertColumnInOrder = useCallback( (anchorColumn: string, newColumn: string, side: 'left' | 'right') => { - const order = columnOrderRef.current - if (!order) return + const order = columnOrderRef.current ?? schemaColumnsRef.current.map((c) => c.name) const newOrder = [...order] let anchorIdx = newOrder.indexOf(anchorColumn) if (anchorIdx === -1) { @@ -1434,12 +1432,12 @@ export function Table({ const handleDeleteColumnConfirm = useCallback(() => { if (!deletingColumn) return const columnToDelete = deletingColumn + const orderAtDelete = columnOrderRef.current setDeletingColumn(null) deleteColumnMutation.mutate(columnToDelete, { onSuccess: () => { - const order = columnOrderRef.current - if (!order) return - const newOrder = order.filter((n) => n !== columnToDelete) + if (!orderAtDelete) return + const newOrder = orderAtDelete.filter((n) => n !== columnToDelete) setColumnOrder(newOrder) updateMetadataRef.current({ columnWidths: columnWidthsRef.current, diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx index a21344fe3f1..07062fc1081 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx @@ -3,7 +3,7 @@ import { useCallback, useMemo } from 'react' import { useQueryClient } from '@tanstack/react-query' import { useParams, usePathname, useRouter } from 'next/navigation' -import { ChevronDown, Skeleton, Tooltip } from '@/components/emcn' +import { ChevronDown, Skeleton } from '@/components/emcn' import { useSession } from '@/lib/auth/auth-client' import { getSubscriptionAccessState } from '@/lib/billing/client' import { isHosted } from '@/lib/core/config/feature-flags' @@ -15,6 +15,7 @@ import { isBillingEnabled, sectionConfig, } from '@/app/workspace/[workspaceId]/settings/navigation' +import { SidebarTooltip } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar' import { useSSOProviders } from '@/ee/sso/hooks/sso' import { prefetchWorkspaceCredentials } from '@/hooks/queries/credentials' import { prefetchGeneralSettings, useGeneralSettings } from '@/hooks/queries/general-settings' @@ -186,25 +187,18 @@ export function SettingsSidebar({ <> {/* Back button */}
- - - - - {showCollapsedTooltips && ( - -

Back

-
- )} -
+ + +
{/* Settings sections */} @@ -303,14 +297,13 @@ export function SettingsSidebar({ ) return ( - - {element} - {showCollapsedTooltips && ( - -

{item.label}

-
- )} -
+ + {element} + ) })}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index 836029439c5..f06f05d7dbb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -341,12 +341,8 @@ export const Sidebar = memo(function Sidebar() { useLayoutEffect(() => { if (!_hasHydrated) return - if (isCollapsed) { - document.documentElement.setAttribute('data-sidebar-collapsed', '') - } else { - document.documentElement.removeAttribute('data-sidebar-collapsed') - } - }, [isCollapsed, _hasHydrated]) + document.documentElement.removeAttribute('data-sidebar-collapsed') + }, [_hasHydrated]) useEffect(() => { if (isCollapsed) { @@ -1221,12 +1217,12 @@ export const Sidebar = memo(function Sidebar() { {/* Top bar: Logo + Collapse toggle */}
- +
{brand.logoUrl ? ( {brand.name} ) : ( - <> - - - + )} - - + + + {brand.logoUrl ? ( + + ) : ( + + )} + + + +