From 3865ee7f9eb63742678bb6814cb6af6de6d6b9da Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 31 Mar 2026 00:59:27 -0700 Subject: [PATCH 1/6] improvement(ui): fix nav loading flash, skeleton mismatches, and React anti-patterns across resource pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Convert knowledge, files, tables, scheduled-tasks, and home page.tsx files from async server components to simple client re-exports, eliminating the loading.tsx flash on every navigation - Add client-side permission redirects (usePermissionConfig) to knowledge, files, and tables components to replace server-side checks - Fix knowledge loading.tsx skeleton column count (6→7) and tables loading.tsx (remove phantom checkbox column) - Fix connector document live updates: use isConnectorSyncingOrPending instead of status === 'syncing' so polling activates immediately after connector creation - Remove dead chunk-switch useEffect in ChunkEditor (redundant with key prop remount) - Replace useState+useEffect debounce with useDebounce hook in document.tsx - Replace useRef+useEffect URL init with lazy useState initializers in document.tsx and logs.tsx - Make handleToggleEnabled optimistic in document.tsx (cache first, onError rollback) - Replace mutate+new Promise wrapper with mutateAsync+try/catch in base.tsx - Fix schedule-modal.tsx: replace 15-setter useEffect with useState lazy initializers + key prop remount; wrap parseCronToScheduleType in useMemo - Fix logs search: eliminate mount-only useEffect with eslint-disable by passing initialQuery to useSearchState; parse query once via shared initialParsed state - Add useWorkspaceFileRecord hook to workspace-files.ts; refactor FileViewer to self-fetch - Fix value: any → value: string in useTagSelection and collaborativeSetTagSelection - Fix knowledge-tag-filters.tsx: pass '' instead of null when filters are cleared (type safety) --- .../[workspaceId]/files/[fileId]/page.tsx | 32 +--------- .../files/[fileId]/view/file-viewer.tsx | 19 ++++-- .../files/[fileId]/view/page.tsx | 41 +------------ .../workspace/[workspaceId]/files/files.tsx | 8 +++ .../workspace/[workspaceId]/files/page.tsx | 31 +--------- .../app/workspace/[workspaceId]/home/page.tsx | 25 +------- .../components/chunk-editor/chunk-editor.tsx | 11 ---- .../knowledge/[id]/[documentId]/document.tsx | 54 ++++------------- .../[workspaceId]/knowledge/[id]/base.tsx | 38 ++++-------- .../[workspaceId]/knowledge/knowledge.tsx | 10 +++- .../[workspaceId]/knowledge/loading.tsx | 2 +- .../[workspaceId]/knowledge/page.tsx | 31 +--------- .../logs-toolbar/components/search/search.tsx | 9 +-- .../logs/hooks/use-search-state.ts | 12 +++- .../app/workspace/[workspaceId]/logs/logs.tsx | 19 +++--- .../create-schedule-modal/schedule-modal.tsx | 59 ++++++++----------- .../[workspaceId]/scheduled-tasks/page.tsx | 25 +------- .../scheduled-tasks/scheduled-tasks.tsx | 1 + .../[workspaceId]/tables/loading.tsx | 6 -- .../workspace/[workspaceId]/tables/page.tsx | 31 +--------- .../workspace/[workspaceId]/tables/tables.tsx | 11 +++- .../knowledge-tag-filters.tsx | 2 +- .../workflow-item/workflow-item.tsx | 5 +- .../workspace-header/workspace-header.tsx | 13 +++- .../w/components/sidebar/sidebar.tsx | 13 ++-- apps/sim/hooks/kb/use-tag-selection.ts | 2 +- apps/sim/hooks/queries/workspace-files.ts | 14 +++++ apps/sim/hooks/use-collaborative-workflow.ts | 2 +- 28 files changed, 153 insertions(+), 373 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/[fileId]/page.tsx b/apps/sim/app/workspace/[workspaceId]/files/[fileId]/page.tsx index 0e8dc9b4758..81398b7f17b 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/[fileId]/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/[fileId]/page.tsx @@ -1,8 +1,4 @@ import type { Metadata } from 'next' -import { redirect } from 'next/navigation' -import { getSession } from '@/lib/auth' -import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' -import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check' import { Files } from '../files' export const metadata: Metadata = { @@ -10,30 +6,4 @@ export const metadata: Metadata = { robots: { index: false }, } -interface FileDetailPageProps { - params: Promise<{ - workspaceId: string - fileId: string - }> -} - -export default async function FileDetailPage({ params }: FileDetailPageProps) { - const { workspaceId } = await params - const session = await getSession() - - if (!session?.user?.id) { - redirect('/') - } - - const hasPermission = await verifyWorkspaceMembership(session.user.id, workspaceId) - if (!hasPermission) { - redirect('/') - } - - const permissionConfig = await getUserPermissionConfig(session.user.id) - if (permissionConfig?.hideFilesTab) { - redirect(`/workspace/${workspaceId}`) - } - - return -} +export default Files diff --git a/apps/sim/app/workspace/[workspaceId]/files/[fileId]/view/file-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/files/[fileId]/view/file-viewer.tsx index b939d50898d..a450bd374da 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/[fileId]/view/file-viewer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/[fileId]/view/file-viewer.tsx @@ -1,15 +1,22 @@ 'use client' import { createLogger } from '@sim/logger' -import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' +import { useParams } from 'next/navigation' +import { useWorkspaceFileRecord } from '@/hooks/queries/workspace-files' const logger = createLogger('FileViewer') -interface FileViewerProps { - file: WorkspaceFileRecord -} +export function FileViewer() { + const params = useParams() + const workspaceId = params?.workspaceId as string + const fileId = params?.fileId as string + + const { data: file, isLoading } = useWorkspaceFileRecord(workspaceId, fileId) + + if (isLoading || !file) { + return null + } -export function FileViewer({ file }: FileViewerProps) { const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace` return ( @@ -18,7 +25,7 @@ export function FileViewer({ file }: FileViewerProps) { src={serveUrl} className='h-full w-full border-0' title={file.name} - onError={(e) => { + onError={() => { logger.error(`Failed to load file: ${file.name}`) }} /> diff --git a/apps/sim/app/workspace/[workspaceId]/files/[fileId]/view/page.tsx b/apps/sim/app/workspace/[workspaceId]/files/[fileId]/view/page.tsx index dc8a2246bee..92aa310135d 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/[fileId]/view/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/[fileId]/view/page.tsx @@ -1,46 +1,9 @@ import type { Metadata } from 'next' -import { redirect, unstable_rethrow } from 'next/navigation' -import { getSession } from '@/lib/auth' -import { getWorkspaceFile } from '@/lib/uploads/contexts/workspace' -import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' -import { FileViewer } from '@/app/workspace/[workspaceId]/files/[fileId]/view/file-viewer' +import { FileViewer } from './file-viewer' export const metadata: Metadata = { title: 'File', robots: { index: false }, } -interface FileViewerPageProps { - params: Promise<{ - workspaceId: string - fileId: string - }> -} - -export default async function FileViewerPage({ params }: FileViewerPageProps) { - const { workspaceId, fileId } = await params - - const session = await getSession() - if (!session?.user?.id) { - redirect('/') - } - - const hasPermission = await verifyWorkspaceMembership(session.user.id, workspaceId) - if (!hasPermission) { - redirect(`/workspace/${workspaceId}`) - } - - let fileRecord: Awaited> - try { - fileRecord = await getWorkspaceFile(workspaceId, fileId) - } catch (error) { - unstable_rethrow(error) - redirect(`/workspace/${workspaceId}`) - } - - if (!fileRecord) { - redirect(`/workspace/${workspaceId}`) - } - - return -} +export default FileViewer diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 16be4f3e8cd..a4c82e1672e 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -75,6 +75,7 @@ import { } from '@/hooks/queries/workspace-files' import { useDebounce } from '@/hooks/use-debounce' import { useInlineRename } from '@/hooks/use-inline-rename' +import { usePermissionConfig } from '@/hooks/use-permission-config' type SaveStatus = 'idle' | 'saving' | 'saved' | 'error' @@ -136,6 +137,13 @@ export function Files() { const fileIdFromRoute = typeof params?.fileId === 'string' && params.fileId.length > 0 ? params.fileId : null const userPermissions = useUserPermissionsContext() + const { config: permissionConfig } = usePermissionConfig() + + useEffect(() => { + if (permissionConfig.hideFilesTab) { + router.replace(`/workspace/${workspaceId}`) + } + }, [permissionConfig.hideFilesTab, router, workspaceId]) const { data: files = [], isLoading, error } = useWorkspaceFiles(workspaceId) const { data: members } = useWorkspaceMembersQuery(workspaceId) diff --git a/apps/sim/app/workspace/[workspaceId]/files/page.tsx b/apps/sim/app/workspace/[workspaceId]/files/page.tsx index 31204696522..43db21afa32 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/page.tsx @@ -1,8 +1,4 @@ import type { Metadata } from 'next' -import { redirect } from 'next/navigation' -import { getSession } from '@/lib/auth' -import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' -import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check' import { Files } from './files' export const metadata: Metadata = { @@ -10,29 +6,4 @@ export const metadata: Metadata = { robots: { index: false }, } -interface FilesPageProps { - params: Promise<{ - workspaceId: string - }> -} - -export default async function FilesPage({ params }: FilesPageProps) { - const { workspaceId } = await params - const session = await getSession() - - if (!session?.user?.id) { - redirect('/') - } - - const hasPermission = await verifyWorkspaceMembership(session.user.id, workspaceId) - if (!hasPermission) { - redirect('/') - } - - const permissionConfig = await getUserPermissionConfig(session.user.id) - if (permissionConfig?.hideFilesTab) { - redirect(`/workspace/${workspaceId}`) - } - - return -} +export default Files diff --git a/apps/sim/app/workspace/[workspaceId]/home/page.tsx b/apps/sim/app/workspace/[workspaceId]/home/page.tsx index 659b6e6865e..f56a5d2124a 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/page.tsx @@ -1,31 +1,8 @@ import type { Metadata } from 'next' -import { redirect } from 'next/navigation' -import { getSession } from '@/lib/auth' -import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' import { Home } from './home' export const metadata: Metadata = { title: 'Home', } -interface HomePageProps { - params: Promise<{ - workspaceId: string - }> -} - -export default async function HomePage({ params }: HomePageProps) { - const { workspaceId } = await params - const session = await getSession() - - if (!session?.user?.id) { - redirect('/') - } - - const hasPermission = await verifyWorkspaceMembership(session.user.id, workspaceId) - if (!hasPermission) { - redirect('/') - } - - return -} +export default Home diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-editor/chunk-editor.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-editor/chunk-editor.tsx index 48133841228..c646fdf15f9 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-editor/chunk-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-editor/chunk-editor.tsx @@ -56,22 +56,11 @@ export function ChunkEditor({ const [savedContent, setSavedContent] = useState(chunkContent) const [tokenizerOn, setTokenizerOn] = useState(false) const [hoveredTokenIndex, setHoveredTokenIndex] = useState(null) - const prevChunkIdRef = useRef(chunk?.id) const savedContentRef = useRef(chunkContent) const editedContentRef = useRef(editedContent) editedContentRef.current = editedContent - useEffect(() => { - if (isCreateMode) return - if (chunk?.id !== prevChunkIdRef.current) { - prevChunkIdRef.current = chunk?.id - savedContentRef.current = chunkContent - setSavedContent(chunkContent) - setEditedContent(chunkContent) - } - }, [isCreateMode, chunk?.id, chunkContent]) - useEffect(() => { if (isCreateMode || !chunk?.id) return const controller = new AbortController() 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 f43c84dbdde..a7c0c48ba59 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx @@ -1,6 +1,6 @@ 'use client' -import { startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { ChevronDown, ChevronUp, FileText, Pencil, Tag } from 'lucide-react' import { useParams, useRouter, useSearchParams } from 'next/navigation' @@ -47,6 +47,7 @@ import { useUpdateChunk, useUpdateDocument, } from '@/hooks/queries/kb/knowledge' +import { useDebounce } from '@/hooks/use-debounce' import { useInlineRename } from '@/hooks/use-inline-rename' const logger = createLogger('Document') @@ -152,7 +153,7 @@ export function Document({ const [showTagsModal, setShowTagsModal] = useState(false) const [searchQuery, setSearchQuery] = useState('') - const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('') + const debouncedSearchQuery = useDebounce(searchQuery, 200) const [enabledFilter, setEnabledFilter] = useState([]) const [activeSort, setActiveSort] = useState<{ column: string @@ -168,11 +169,8 @@ export function Document({ chunks: initialChunks, currentPage: initialPage, totalPages: initialTotalPages, - hasNextPage: initialHasNextPage, - hasPrevPage: initialHasPrevPage, goToPage: initialGoToPage, error: initialError, - refreshChunks: initialRefreshChunks, updateChunk: initialUpdateChunk, isFetching: isFetchingChunks, } = useDocumentChunks( @@ -207,7 +205,9 @@ export function Document({ const [selectedChunks, setSelectedChunks] = useState>(() => new Set()) // Inline editor state - const [selectedChunkId, setSelectedChunkId] = useState(null) + const [selectedChunkId, setSelectedChunkId] = useState(() => + searchParams.get('chunk') + ) const [isCreatingNewChunk, setIsCreatingNewChunk] = useState(false) const [isDirty, setIsDirty] = useState(false) const [saveStatus, setSaveStatus] = useState('idle') @@ -217,27 +217,6 @@ export function Document({ const saveStatusRef = useRef('idle') saveStatusRef.current = saveStatus - // Auto-select chunk from URL param on mount - const initialChunkParam = useRef(searchParams.get('chunk')) - useEffect(() => { - if (initialChunkParam.current) { - setSelectedChunkId(initialChunkParam.current) - initialChunkParam.current = null - } - }, []) - - useEffect(() => { - const handler = setTimeout(() => { - startTransition(() => { - setDebouncedSearchQuery(searchQuery) - }) - }, 200) - - return () => { - clearTimeout(handler) - } - }, [searchQuery]) - const isSearching = debouncedSearchQuery.trim().length > 0 const showingSearch = isSearching && searchQuery.trim().length > 0 && searchResults.length > 0 const SEARCH_PAGE_SIZE = 50 @@ -259,8 +238,6 @@ export function Document({ const currentPage = showingSearch ? searchCurrentPage : initialPage const totalPages = showingSearch ? searchTotalPages : initialTotalPages - const hasNextPage = showingSearch ? searchCurrentPage < searchTotalPages : initialHasNextPage - const hasPrevPage = showingSearch ? searchCurrentPage > 1 : initialHasPrevPage // Keep refs to displayChunks and totalPages so polling callbacks can read fresh data const displayChunksRef = useRef(displayChunks) @@ -281,12 +258,11 @@ export function Document({ if (showingSearch) { return } - return await initialGoToPage(page) + return initialGoToPage(page) }, [showingSearch, initialGoToPage] ) - const refreshChunks = showingSearch ? async () => {} : initialRefreshChunks const updateChunk = showingSearch ? (_id: string, _updates: Record) => {} : initialUpdateChunk @@ -309,7 +285,6 @@ export function Document({ const { isOpen: isContextMenuOpen, position: contextMenuPosition, - menuRef, handleContextMenu: baseHandleContextMenu, closeMenu: closeContextMenu, } = useContextMenu() @@ -661,18 +636,11 @@ export function Document({ const chunk = displayChunks.find((c) => c.id === chunkId) if (!chunk) return + const newEnabled = !chunk.enabled + updateChunk(chunkId, { enabled: newEnabled }) updateChunkMutation( - { - knowledgeBaseId, - documentId, - chunkId, - enabled: !chunk.enabled, - }, - { - onSuccess: () => { - updateChunk(chunkId, { enabled: !chunk.enabled }) - }, - } + { knowledgeBaseId, documentId, chunkId, enabled: newEnabled }, + { onError: () => updateChunk(chunkId, { enabled: chunk.enabled }) } ) }, [displayChunks, knowledgeBaseId, documentId, updateChunk] diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index a8d6a80ca83..69150737971 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -62,7 +62,7 @@ import { type TagDefinition, useKnowledgeBaseTagDefinitions, } from '@/hooks/kb/use-knowledge-base-tag-definitions' -import { useConnectorList } from '@/hooks/queries/kb/connectors' +import { isConnectorSyncingOrPending, useConnectorList } from '@/hooks/queries/kb/connectors' import type { DocumentTagFilter } from '@/hooks/queries/kb/knowledge' import { useBulkDocumentOperation, @@ -194,7 +194,7 @@ export function KnowledgeBase({ const { removeKnowledgeBase } = useKnowledgeBasesList(workspaceId, { enabled: false }) const userPermissions = useUserPermissionsContext() - const { mutate: updateDocumentMutation } = useUpdateDocument() + const { mutate: updateDocumentMutation, mutateAsync: updateDocumentAsync } = useUpdateDocument() const { mutate: deleteDocumentMutation } = useDeleteDocument() const { mutate: deleteKnowledgeBaseMutation, isPending: isDeleting } = useDeleteKnowledgeBase(workspaceId) @@ -285,7 +285,7 @@ export function KnowledgeBase({ } = useKnowledgeBase(id) const { data: connectors = [], isLoading: isLoadingConnectors } = useConnectorList(id) - const hasSyncingConnectors = connectors.some((c) => c.status === 'syncing') + const hasSyncingConnectors = connectors.some(isConnectorSyncingOrPending) const hasSyncingConnectorsRef = useRef(hasSyncingConnectors) hasSyncingConnectorsRef.current = hasSyncingConnectors @@ -455,28 +455,16 @@ export function KnowledgeBase({ updateDocument(documentId, { filename: newName }) - return new Promise((resolve, reject) => { - updateDocumentMutation( - { - knowledgeBaseId: id, - documentId, - updates: { filename: newName }, - }, - { - onSuccess: () => { - logger.info(`Document renamed: ${documentId}`) - resolve() - }, - onError: (err) => { - if (previousName !== undefined) { - updateDocument(documentId, { filename: previousName }) - } - logger.error('Error renaming document:', err) - reject(err) - }, - } - ) - }) + try { + await updateDocumentAsync({ knowledgeBaseId: id, documentId, updates: { filename: newName } }) + logger.info(`Document renamed: ${documentId}`) + } catch (err) { + if (previousName !== undefined) { + updateDocument(documentId, { filename: previousName }) + } + logger.error('Error renaming document:', err) + throw err + } } /** diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx index 58bd4ceddae..f844fa9628e 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { useParams, useRouter } from 'next/navigation' import type { ComboboxOption } from '@/components/emcn' @@ -33,6 +33,7 @@ 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' +import { usePermissionConfig } from '@/hooks/use-permission-config' const logger = createLogger('Knowledge') @@ -91,6 +92,13 @@ export function Knowledge() { const router = useRouter() const workspaceId = params.workspaceId as string + const { config: permissionConfig } = usePermissionConfig() + useEffect(() => { + if (permissionConfig.hideKnowledgeBaseTab) { + router.replace(`/workspace/${workspaceId}`) + } + }, [permissionConfig.hideKnowledgeBaseTab, router, workspaceId]) + const { knowledgeBases, isLoading, error } = useKnowledgeBasesList(workspaceId) const { data: members } = useWorkspaceMembersQuery(workspaceId) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/loading.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/loading.tsx index 9011fcccca6..dbfd329e93d 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/loading.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/loading.tsx @@ -1,7 +1,7 @@ import { Skeleton } from '@/components/emcn' const SKELETON_ROW_COUNT = 5 -const COLUMN_COUNT = 6 +const COLUMN_COUNT = 7 export default function KnowledgeLoading() { return ( diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/page.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/page.tsx index d52602721a6..be3743be659 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/page.tsx @@ -1,37 +1,8 @@ import type { Metadata } from 'next' -import { redirect } from 'next/navigation' -import { getSession } from '@/lib/auth' -import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' -import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check' import { Knowledge } from './knowledge' export const metadata: Metadata = { title: 'Knowledge Base', } -interface KnowledgePageProps { - params: Promise<{ - workspaceId: string - }> -} - -export default async function KnowledgePage({ params }: KnowledgePageProps) { - const { workspaceId } = await params - const session = await getSession() - - if (!session?.user?.id) { - redirect('/') - } - - const hasPermission = await verifyWorkspaceMembership(session.user.id, workspaceId) - if (!hasPermission) { - redirect('/') - } - - const permissionConfig = await getUserPermissionConfig(session.user.id) - if (permissionConfig?.hideKnowledgeBaseTab) { - redirect(`/workspace/${workspaceId}`) - } - - return -} +export default Knowledge diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/search/search.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/search/search.tsx index cb8b795276b..2fa5a48e25c 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/search/search.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/search/search.tsx @@ -103,6 +103,7 @@ export function AutocompleteSearch({ } = useSearchState({ onFiltersChange: handleFiltersChange, getSuggestions: (input) => suggestionEngine.getSuggestions(input), + initialQuery: value, }) const lastExternalValue = useRef(value) @@ -114,14 +115,6 @@ export function AutocompleteSearch({ } }, [value, initializeFromQuery]) - useEffect(() => { - if (value) { - const parsed = parseQuery(value) - initializeFromQuery(parsed.textSearch, parsed.filters) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - const [dropdownWidth, setDropdownWidth] = useState(400) useEffect(() => { const measure = () => { diff --git a/apps/sim/app/workspace/[workspaceId]/logs/hooks/use-search-state.ts b/apps/sim/app/workspace/[workspaceId]/logs/hooks/use-search-state.ts index 1e4647dec56..19af1dd346f 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/hooks/use-search-state.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/hooks/use-search-state.ts @@ -1,5 +1,6 @@ import { useCallback, useRef, useState } from 'react' import type { ParsedFilter } from '@/lib/logs/query-parser' +import { parseQuery } from '@/lib/logs/query-parser' import type { Suggestion, SuggestionGroup, @@ -10,16 +11,21 @@ interface UseSearchStateOptions { onFiltersChange: (filters: ParsedFilter[], textSearch: string) => void getSuggestions: (input: string) => SuggestionGroup | null debounceMs?: number + initialQuery?: string } export function useSearchState({ onFiltersChange, getSuggestions, debounceMs = 100, + initialQuery, }: UseSearchStateOptions) { - const [appliedFilters, setAppliedFilters] = useState([]) + const [initialParsed] = useState(() => + initialQuery ? parseQuery(initialQuery) : { filters: [] as ParsedFilter[], textSearch: '' } + ) + const [appliedFilters, setAppliedFilters] = useState(initialParsed.filters) const [currentInput, setCurrentInput] = useState('') - const [textSearch, setTextSearch] = useState('') + const [textSearch, setTextSearch] = useState(initialParsed.textSearch) const [isOpen, setIsOpen] = useState(false) const [suggestions, setSuggestions] = useState([]) @@ -84,7 +90,7 @@ export function useSearchState({ } const newFilter: ParsedFilter = { - field: suggestion.value.split(':')[0] as any, + field: suggestion.value.split(':')[0], operator: '=', value: suggestion.value.includes(':') ? suggestion.value.split(':').slice(1).join(':').replace(/"/g, '') diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index 098f23158a8..578778cada4 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -266,19 +266,18 @@ export default function Logs() { isSidebarOpen: false, }) const isInitialized = useRef(false) - const pendingExecutionIdRef = useRef(null) + const pendingExecutionIdRef = useRef( + typeof window !== 'undefined' + ? new URLSearchParams(window.location.search).get('executionId') + : null + ) - const [searchQuery, setSearchQuery] = useState('') + const [searchQuery, setSearchQuery] = useState(() => { + if (typeof window === 'undefined') return '' + return new URLSearchParams(window.location.search).get('search') ?? '' + }) const debouncedSearchQuery = useDebounce(searchQuery, 300) - useEffect(() => { - const params = new URLSearchParams(window.location.search) - const urlSearch = params.get('search') - if (urlSearch) setSearchQuery(urlSearch) - const urlExecutionId = params.get('executionId') - if (urlExecutionId) pendingExecutionIdRef.current = urlExecutionId - }, []) - const isLive = true const [isVisuallyRefreshing, setIsVisuallyRefreshing] = useState(false) const [isExporting, setIsExporting] = useState(false) diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx index abcafb2974c..2449cce4a3d 100644 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useMemo, useState } from 'react' +import { useMemo, useState } from 'react' import { createLogger } from '@sim/logger' import { Button, @@ -157,43 +157,32 @@ export function ScheduleModal({ open, onOpenChange, workspaceId, schedule }: Sch const isEditing = Boolean(schedule) - const [title, setTitle] = useState('') - const [prompt, setPrompt] = useState('') - const [scheduleType, setScheduleType] = useState('daily') - const [minutesInterval, setMinutesInterval] = useState('15') - const [hourlyMinute, setHourlyMinute] = useState('0') - const [dailyTime, setDailyTime] = useState('09:00') - const [weeklyDay, setWeeklyDay] = useState('MON') - const [weeklyDayTime, setWeeklyDayTime] = useState('09:00') - const [monthlyDay, setMonthlyDay] = useState('1') - const [monthlyTime, setMonthlyTime] = useState('09:00') - const [cronExpression, setCronExpression] = useState('') - const [timezone, setTimezone] = useState(DEFAULT_TIMEZONE) + const initialCronState = useMemo( + () => (schedule ? parseCronToScheduleType(schedule.cronExpression) : null), + [schedule?.cronExpression] + ) + + const [title, setTitle] = useState(schedule?.jobTitle ?? '') + const [prompt, setPrompt] = useState(schedule?.prompt ?? '') + const [scheduleType, setScheduleType] = useState( + initialCronState?.scheduleType ?? 'daily' + ) + const [minutesInterval, setMinutesInterval] = useState(initialCronState?.minutesInterval ?? '15') + const [hourlyMinute, setHourlyMinute] = useState(initialCronState?.hourlyMinute ?? '0') + const [dailyTime, setDailyTime] = useState(initialCronState?.dailyTime ?? '09:00') + const [weeklyDay, setWeeklyDay] = useState(initialCronState?.weeklyDay ?? 'MON') + const [weeklyDayTime, setWeeklyDayTime] = useState(initialCronState?.weeklyDayTime ?? '09:00') + const [monthlyDay, setMonthlyDay] = useState(initialCronState?.monthlyDay ?? '1') + const [monthlyTime, setMonthlyTime] = useState(initialCronState?.monthlyTime ?? '09:00') + const [cronExpression, setCronExpression] = useState(initialCronState?.cronExpression ?? '') + const [timezone, setTimezone] = useState(schedule?.timezone ?? DEFAULT_TIMEZONE) const [startDate, setStartDate] = useState('') - const [lifecycle, setLifecycle] = useState<'persistent' | 'until_complete'>('persistent') - const [maxRuns, setMaxRuns] = useState('') + const [lifecycle, setLifecycle] = useState<'persistent' | 'until_complete'>( + schedule?.lifecycle === 'until_complete' ? 'until_complete' : 'persistent' + ) + const [maxRuns, setMaxRuns] = useState(schedule?.maxRuns ? String(schedule.maxRuns) : '') const [submitError, setSubmitError] = useState(null) - useEffect(() => { - if (!open || !schedule) return - const cronState = parseCronToScheduleType(schedule.cronExpression) - setTitle(schedule.jobTitle || '') - setPrompt(schedule.prompt || '') - setScheduleType(cronState.scheduleType) - setMinutesInterval(cronState.minutesInterval) - setHourlyMinute(cronState.hourlyMinute) - setDailyTime(cronState.dailyTime) - setWeeklyDay(cronState.weeklyDay) - setWeeklyDayTime(cronState.weeklyDayTime) - setMonthlyDay(cronState.monthlyDay) - setMonthlyTime(cronState.monthlyTime) - setCronExpression(cronState.cronExpression) - setTimezone(schedule.timezone || DEFAULT_TIMEZONE) - setLifecycle(schedule.lifecycle === 'until_complete' ? 'until_complete' : 'persistent') - setMaxRuns(schedule.maxRuns ? String(schedule.maxRuns) : '') - setStartDate('') - }, [open, schedule]) - const computedCron = useMemo( () => buildCronExpression(scheduleType, { diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/page.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/page.tsx index 9135138d28e..1f0fadd8bfa 100644 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/page.tsx @@ -1,31 +1,8 @@ import type { Metadata } from 'next' -import { redirect } from 'next/navigation' -import { getSession } from '@/lib/auth' -import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' import { ScheduledTasks } from './scheduled-tasks' export const metadata: Metadata = { title: 'Scheduled Tasks', } -interface ScheduledTasksPageProps { - params: Promise<{ - workspaceId: string - }> -} - -export default async function ScheduledTasksPage({ params }: ScheduledTasksPageProps) { - const { workspaceId } = await params - const session = await getSession() - - if (!session?.user?.id) { - redirect('/') - } - - const hasPermission = await verifyWorkspaceMembership(session.user.id, workspaceId) - if (!hasPermission) { - redirect('/') - } - - return -} +export default ScheduledTasks 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 2fb6fb3e47c..69165d11798 100644 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx +++ b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx @@ -433,6 +433,7 @@ export function ScheduledTasks() { /> { setIsEditModalOpen(open) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/loading.tsx b/apps/sim/app/workspace/[workspaceId]/tables/loading.tsx index eda84842abf..a0c2aa9068b 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/loading.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/loading.tsx @@ -27,9 +27,6 @@ export default function TablesLoading() { - {Array.from({ length: COLUMN_COUNT }).map((_, i) => ( {Array.from({ length: SKELETON_ROW_COUNT }).map((_, rowIndex) => ( - {Array.from({ length: COLUMN_COUNT }).map((_, colIndex) => (
- - @@ -40,9 +37,6 @@ export default function TablesLoading() {
- - -} - -export default async function TablesPage({ params }: TablesPageProps) { - const { workspaceId } = await params - const session = await getSession() - - if (!session?.user?.id) { - redirect('/') - } - - const hasPermission = await verifyWorkspaceMembership(session.user.id, workspaceId) - if (!hasPermission) { - redirect('/') - } - - const permissionConfig = await getUserPermissionConfig(session.user.id) - if (permissionConfig?.hideTablesTab) { - redirect(`/workspace/${workspaceId}`) - } - - return -} +export default Tables diff --git a/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx b/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx index 95ed0801d9f..a26aa3a18ed 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { useParams, useRouter } from 'next/navigation' import type { ComboboxOption } from '@/components/emcn' @@ -38,6 +38,7 @@ import { } from '@/hooks/queries/tables' import { useWorkspaceMembersQuery } from '@/hooks/queries/workspace' import { useDebounce } from '@/hooks/use-debounce' +import { usePermissionConfig } from '@/hooks/use-permission-config' const logger = createLogger('Tables') @@ -54,6 +55,14 @@ export function Tables() { const params = useParams() const router = useRouter() const workspaceId = params.workspaceId as string + + const { config: permissionConfig } = usePermissionConfig() + useEffect(() => { + if (permissionConfig.hideTablesTab) { + router.replace(`/workspace/${workspaceId}`) + } + }, [permissionConfig.hideTablesTab, router, workspaceId]) + const userPermissions = useUserPermissionsContext() const { data: tables = [], isLoading, error } = useTablesList(workspaceId) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx index ad3c5478845..5963ec9a829 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx @@ -196,7 +196,7 @@ export function KnowledgeTagFilters({ if (isReadOnly) return const updatedFilters = filters.map((f) => (f.id === id ? { ...f, [field]: value } : f)) - const jsonValue = updatedFilters.length > 0 ? JSON.stringify(updatedFilters) : null + const jsonValue = updatedFilters.length > 0 ? JSON.stringify(updatedFilters) : '' emitTagSelection(jsonValue) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx index 37f276ff686..07185435df3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx @@ -388,12 +388,13 @@ export function WorkflowItem({ data-item-id={workflow.id} className={clsx( 'group mx-0.5 flex h-[30px] items-center gap-2 rounded-lg px-2 text-sm', - (active || isContextMenuOpen) && 'bg-[var(--surface-active)]', + (active || isContextMenuOpen || (isSelected && selectedWorkflows.size > 1)) && + 'bg-[var(--surface-active)]', !active && !isContextMenuOpen && + !(isSelected && selectedWorkflows.size > 1) && !isAnyDragActive && 'hover-hover:bg-[var(--surface-hover)]', - isSelected && selectedWorkflows.size > 1 && !active && 'bg-[var(--surface-active)]', (isDragging || (isAnyDragActive && isSelected)) && 'opacity-50' )} draggable={!isEditing && !dragDisabled} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx index fe33dc9ab00..93defdd2d05 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx @@ -120,6 +120,7 @@ export function WorkspaceHeader({ const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }) const [isContextMenuOpen, setIsContextMenuOpen] = useState(false) + const [menuOpenWorkspaceId, setMenuOpenWorkspaceId] = useState(null) const contextMenuRef = useRef(null) const capturedWorkspaceRef = useRef(null) const isRenamingRef = useRef(false) @@ -185,6 +186,7 @@ export function WorkspaceHeader({ contextMenuClosedRef.current = false capturedWorkspaceRef.current = workspace + setMenuOpenWorkspaceId(workspace.id) setContextMenuPosition({ x, y }) setIsContextMenuOpen(true) } @@ -210,6 +212,7 @@ export function WorkspaceHeader({ contextMenuClosedRef.current = true setIsContextMenuOpen(false) + setMenuOpenWorkspaceId(null) const isOpeningAnother = isContextMenuOpeningRef.current isContextMenuOpeningRef.current = false if (!isRenamingRef.current && !isOpeningAnother) { @@ -494,8 +497,11 @@ export function WorkspaceHeader({ className={cn( 'group flex cursor-pointer select-none items-center gap-2 rounded-[5px] px-2 py-[5px] font-medium text-[var(--text-body)] text-caption outline-none transition-colors', workspace.id !== workspaceId && + menuOpenWorkspaceId !== workspace.id && 'hover-hover:bg-[var(--surface-hover)]', - workspace.id === workspaceId && 'bg-[var(--surface-active)]' + (workspace.id === workspaceId || + menuOpenWorkspaceId === workspace.id) && + 'bg-[var(--surface-active)]' )} onClick={() => onWorkspaceSwitch(workspace)} onContextMenu={(e) => handleContextMenu(e, workspace)} @@ -513,7 +519,10 @@ export function WorkspaceHeader({ const rect = e.currentTarget.getBoundingClientRect() openContextMenuAt(workspace, rect.right, rect.top) }} - className='flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-sm opacity-0 transition-opacity group-hover:opacity-100' + className={cn( + 'flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-sm opacity-0 transition-opacity group-hover:opacity-100', + menuOpenWorkspaceId === workspace.id && 'opacity-100' + )} > 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 7c89b20e191..e27de3a895c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -203,7 +203,7 @@ const SidebarTaskItem = memo(function SidebarTaskItem({ onMoreClick(e, task.id) }} className={cn( - 'flex h-[18px] w-[18px] items-center justify-center rounded-sm opacity-0 group-hover:opacity-100', + 'flex h-[18px] w-[18px] items-center justify-center rounded-sm opacity-0 transition-opacity group-hover:opacity-100', isMenuOpen && 'opacity-100' )} > @@ -1329,11 +1329,8 @@ export const Sidebar = memo(function Sidebar() { !hasOverflowTop && 'border-transparent' )} > -
-
+
+
All tasks
{!isCollapsed && (
@@ -1454,10 +1451,10 @@ export const Sidebar = memo(function Sidebar() {
-
+
Workflows
{!isCollapsed && (
diff --git a/apps/sim/hooks/kb/use-tag-selection.ts b/apps/sim/hooks/kb/use-tag-selection.ts index 37ef53f9b97..490a75f8015 100644 --- a/apps/sim/hooks/kb/use-tag-selection.ts +++ b/apps/sim/hooks/kb/use-tag-selection.ts @@ -9,7 +9,7 @@ export function useTagSelection(blockId: string, subblockId: string) { const { collaborativeSetTagSelection } = useCollaborativeWorkflow() const emitTagSelectionValue = useCallback( - (value: any) => { + (value: string) => { collaborativeSetTagSelection(blockId, subblockId, value) }, [blockId, subblockId, collaborativeSetTagSelection] diff --git a/apps/sim/hooks/queries/workspace-files.ts b/apps/sim/hooks/queries/workspace-files.ts index 074ed3b8c8d..42391a3427a 100644 --- a/apps/sim/hooks/queries/workspace-files.ts +++ b/apps/sim/hooks/queries/workspace-files.ts @@ -33,6 +33,20 @@ export interface StorageInfo { plan?: string } +/** + * Hook to fetch a single workspace file record by ID. + * Uses the full list query so the result is shared from cache when available. + */ +export function useWorkspaceFileRecord(workspaceId: string, fileId: string) { + return useQuery({ + queryKey: workspaceFilesKeys.list(workspaceId, 'all'), + queryFn: ({ signal }) => fetchWorkspaceFiles(workspaceId, 'all', signal), + enabled: !!workspaceId && !!fileId, + staleTime: 30 * 1000, + select: (files) => files.find((f) => f.id === fileId) ?? null, + }) +} + /** * Fetch workspace files from API */ diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts index b8b85b3cbd1..4093bed8b20 100644 --- a/apps/sim/hooks/use-collaborative-workflow.ts +++ b/apps/sim/hooks/use-collaborative-workflow.ts @@ -1281,7 +1281,7 @@ export function useCollaborativeWorkflow() { // Immediate tag selection (uses queue but processes immediately, no debouncing) const collaborativeSetTagSelection = useCallback( - (blockId: string, subblockId: string, value: any) => { + (blockId: string, subblockId: string, value: string) => { if (isApplyingRemoteChange.current) return if (isBaselineDiffView) { From 473cfcc037b651003bde3e173cbf630d9bd07f20 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 31 Mar 2026 01:11:31 -0700 Subject: [PATCH 2/6] fix(kb): use active scope in useWorkspaceFileRecord to share cache with useWorkspaceFiles --- apps/sim/hooks/queries/workspace-files.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/sim/hooks/queries/workspace-files.ts b/apps/sim/hooks/queries/workspace-files.ts index 42391a3427a..dbb50e6b8a1 100644 --- a/apps/sim/hooks/queries/workspace-files.ts +++ b/apps/sim/hooks/queries/workspace-files.ts @@ -35,12 +35,12 @@ export interface StorageInfo { /** * Hook to fetch a single workspace file record by ID. - * Uses the full list query so the result is shared from cache when available. + * Uses the same query key as useWorkspaceFiles so the result is served from cache when the list is already loaded. */ export function useWorkspaceFileRecord(workspaceId: string, fileId: string) { return useQuery({ - queryKey: workspaceFilesKeys.list(workspaceId, 'all'), - queryFn: ({ signal }) => fetchWorkspaceFiles(workspaceId, 'all', signal), + queryKey: workspaceFilesKeys.list(workspaceId, 'active'), + queryFn: ({ signal }) => fetchWorkspaceFiles(workspaceId, 'active', signal), enabled: !!workspaceId && !!fileId, staleTime: 30 * 1000, select: (files) => files.find((f) => f.id === fileId) ?? null, From ac372107bd5af3bfeec3d7ee6bf6609a7ba98679 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 31 Mar 2026 10:34:35 -0700 Subject: [PATCH 3/6] fix(logs,kb,tasks): lazy-init useRef for URL param, add cold-path docs to useWorkspaceFileRecord, document key remount requirement in ScheduleModal --- apps/sim/app/workspace/[workspaceId]/logs/logs.tsx | 12 +++++++----- .../create-schedule-modal/schedule-modal.tsx | 8 ++++++++ apps/sim/hooks/queries/workspace-files.ts | 5 ++++- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index 578778cada4..489c44c3ade 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -266,11 +266,13 @@ export default function Logs() { isSidebarOpen: false, }) const isInitialized = useRef(false) - const pendingExecutionIdRef = useRef( - typeof window !== 'undefined' - ? new URLSearchParams(window.location.search).get('executionId') - : null - ) + const pendingExecutionIdRef = useRef(undefined) + if (pendingExecutionIdRef.current === undefined) { + pendingExecutionIdRef.current = + typeof window !== 'undefined' + ? new URLSearchParams(window.location.search).get('executionId') + : null + } const [searchQuery, setSearchQuery] = useState(() => { if (typeof window === 'undefined') return '' diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx index 2449cce4a3d..11eedd834aa 100644 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx @@ -151,6 +151,14 @@ function buildCronExpression( } } +/** + * Modal for creating and editing scheduled tasks. + * + * All `useState` initializers read from the `schedule` prop at mount time only. + * When editing an existing task, the call-site **must** supply a `key` prop equal to the + * task's ID so React remounts the component when the selected task changes — otherwise + * the form will display stale values from the previously selected task. + */ export function ScheduleModal({ open, onOpenChange, workspaceId, schedule }: ScheduleModalProps) { const createScheduleMutation = useCreateSchedule() const updateScheduleMutation = useUpdateSchedule() diff --git a/apps/sim/hooks/queries/workspace-files.ts b/apps/sim/hooks/queries/workspace-files.ts index dbb50e6b8a1..befea6e7f65 100644 --- a/apps/sim/hooks/queries/workspace-files.ts +++ b/apps/sim/hooks/queries/workspace-files.ts @@ -35,7 +35,10 @@ export interface StorageInfo { /** * Hook to fetch a single workspace file record by ID. - * Uses the same query key as useWorkspaceFiles so the result is served from cache when the list is already loaded. + * Shares the `list(workspaceId, 'active')` query key with {@link useWorkspaceFiles} so no extra + * network request is made when the list is already cached (warm path). + * On a cold path (e.g. direct navigation to a file URL), this fetches the full active file list + * for the workspace and selects the matching record via `select`. */ export function useWorkspaceFileRecord(workspaceId: string, fileId: string) { return useQuery({ From 2af124cbab98de56d8b31e7c44fe6c8186847ac0 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 31 Mar 2026 11:00:01 -0700 Subject: [PATCH 4/6] fix(files): redirect to files list when file record not found in viewer --- .../[workspaceId]/files/[fileId]/view/file-viewer.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/[fileId]/view/file-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/files/[fileId]/view/file-viewer.tsx index a450bd374da..70bc3eb21af 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/[fileId]/view/file-viewer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/[fileId]/view/file-viewer.tsx @@ -1,18 +1,26 @@ 'use client' +import { useEffect } from 'react' import { createLogger } from '@sim/logger' -import { useParams } from 'next/navigation' +import { useParams, useRouter } from 'next/navigation' import { useWorkspaceFileRecord } from '@/hooks/queries/workspace-files' const logger = createLogger('FileViewer') export function FileViewer() { const params = useParams() + const router = useRouter() const workspaceId = params?.workspaceId as string const fileId = params?.fileId as string const { data: file, isLoading } = useWorkspaceFileRecord(workspaceId, fileId) + useEffect(() => { + if (!isLoading && !file) { + router.replace(`/workspace/${workspaceId}/files`) + } + }, [isLoading, file, router, workspaceId]) + if (isLoading || !file) { return null } From 4bf57d7fd80cbccecad8d68a619a7d80613172e1 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 31 Mar 2026 11:02:24 -0700 Subject: [PATCH 5/6] revert(files): remove useEffect redirect from file-viewer, keep simple null return --- .../[workspaceId]/files/[fileId]/view/file-viewer.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/[fileId]/view/file-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/files/[fileId]/view/file-viewer.tsx index 70bc3eb21af..a450bd374da 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/[fileId]/view/file-viewer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/[fileId]/view/file-viewer.tsx @@ -1,26 +1,18 @@ 'use client' -import { useEffect } from 'react' import { createLogger } from '@sim/logger' -import { useParams, useRouter } from 'next/navigation' +import { useParams } from 'next/navigation' import { useWorkspaceFileRecord } from '@/hooks/queries/workspace-files' const logger = createLogger('FileViewer') export function FileViewer() { const params = useParams() - const router = useRouter() const workspaceId = params?.workspaceId as string const fileId = params?.fileId as string const { data: file, isLoading } = useWorkspaceFileRecord(workspaceId, fileId) - useEffect(() => { - if (!isLoading && !file) { - router.replace(`/workspace/${workspaceId}/files`) - } - }, [isLoading, file, router, workspaceId]) - if (isLoading || !file) { return null } From 3d5b7f2d1742e97f7fa8456a6fbffe9b1d317345 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 31 Mar 2026 11:24:43 -0700 Subject: [PATCH 6/6] fix(scheduled-tasks): correct useMemo dep from schedule?.cronExpression to schedule --- .../components/create-schedule-modal/schedule-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx index 11eedd834aa..50806f02a0a 100644 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx @@ -167,7 +167,7 @@ export function ScheduleModal({ open, onOpenChange, workspaceId, schedule }: Sch const initialCronState = useMemo( () => (schedule ? parseCronToScheduleType(schedule.cronExpression) : null), - [schedule?.cronExpression] + [schedule] ) const [title, setTitle] = useState(schedule?.jobTitle ?? '')