Skip to content

improvement(ui): fix nav loading flash, skeleton mismatches, and React anti-patterns across resource pages#3864

Open
waleedlatif1 wants to merge 2 commits intostagingfrom
feat/kb-loading
Open

improvement(ui): fix nav loading flash, skeleton mismatches, and React anti-patterns across resource pages#3864
waleedlatif1 wants to merge 2 commits intostagingfrom
feat/kb-loading

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

Summary

  • Convert knowledge, files, tables, scheduled-tasks, and home `page.tsx` files from async server components to simple client re-exports — eliminates the full-page loading skeleton flash on every navigation between tabs
  • Add client-side permission redirects via `usePermissionConfig` to replace the removed server-side checks in knowledge, files, and tables
  • Fix knowledge loading skeleton column count (6→7); fix tables loading skeleton (remove phantom checkbox column that doesn't exist in the real table)
  • Fix connector documents not appearing live after creation — `isConnectorSyncingOrPending` now used instead of `status === 'syncing'`, so polling activates immediately on connector creation before the first sync runs
  • Remove dead chunk-switch `useEffect` in `ChunkEditor` (made redundant by `key={selectedChunk.id}` remount in parent)
  • Replace `useState` + `useEffect` debounce anti-pattern with `useDebounce` hook in `document.tsx`
  • Replace `useRef` + `useEffect` one-time URL init with lazy `useState` initializers in `document.tsx` and `logs.tsx`
  • Make `handleToggleEnabled` optimistic — cache updates immediately, mutation fires async, `onError` rolls back
  • Replace `mutate` + `new Promise` wrapper with `mutateAsync` + `try/catch` in rename handler
  • Fix `schedule-modal.tsx`: replace 15-setter `useEffect` init with lazy `useState` initializers + `key` prop remount at call site; wrap `parseCronToScheduleType` in `useMemo`
  • Fix logs search: remove mount-only `useEffect` (with `eslint-disable` comment) by passing `initialQuery` to `useSearchState`; parse query string once via shared `initialParsed` state
  • Add `useWorkspaceFileRecord` hook (selects from list cache — zero extra network request when list is already cached); refactor `FileViewer` to self-fetch
  • Fix `value: any` → `value: string` in `useTagSelection` and `collaborativeSetTagSelection`; fix `knowledge-tag-filters.tsx` to pass `''` instead of `null` when filters are cleared
  • Sidebar: fix workflow item active highlight to include multi-select state; fix workspace header context menu to keep item highlighted while menu is open; add `transition-opacity` consistency to task items

Type of Change

  • Bug fix
  • Improvement

Testing

Tested manually

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…t anti-patterns across resource pages

- 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)
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 31, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Mar 31, 2026 8:11am

Request Review

@cursor
Copy link
Copy Markdown

cursor bot commented Mar 31, 2026

PR Summary

Medium Risk
Moderate risk because server-side session/membership/permission redirects were removed from multiple page.tsx routes and replaced with client-side redirects, which could change access-control behavior if upstream API enforcement is incomplete.

Overview
Removes async server logic from multiple workspace page.tsx routes (Home, Files, Tables, Knowledge, Scheduled Tasks, and file subroutes) and re-exports client components instead, reducing full-page loading flashes during tab navigation.

Shifts tab-hiding behavior to the client via usePermissionConfig() redirects (Files/Knowledge/Tables) and refactors file viewing to self-fetch via a new cached selector hook useWorkspaceFileRecord().

Cleans up several UI/state patterns: replaces mount/init useEffect debounces with useDebounce/lazy state (Knowledge document view + Logs search), makes chunk enable/disable optimistic with rollback on error, fixes connector “syncing/pending” detection for polling, simplifies schedule edit initialization (plus key remount), corrects skeleton column mismatches, and tweaks sidebar/workspace header highlighting/hover behavior.

Tightens types around tag-selection plumbing (anystring) and ensures cleared knowledge tag filters emit an empty string instead of null.

Written by Cursor Bugbot for commit 473cfcc. Configure here.

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

greptile

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

typeof window !== 'undefined'
? new URLSearchParams(window.location.search).get('executionId')
: null
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useRef re-evaluates URL parsing on every render

Low Severity

useRef does not support lazy initialization like useState(() => ...). The expression new URLSearchParams(window.location.search).get('executionId') is evaluated on every render, allocating a throwaway URLSearchParams object each time, even though only the first render's value is used by the ref. The searchQuery useState on line 275 correctly uses a lazy initializer for the same pattern, but pendingExecutionIdRef does not get the same treatment.

Fix in Cursor Fix in Web

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 31, 2026

Greptile Summary

This PR introduces a broad set of improvements across resource pages (knowledge, files, tables, scheduled-tasks, home) in the Sim workspace. The central change converts async server-component page.tsx files to simple client re-exports, eliminating the full-page skeleton flash on navigation. Permission-based redirects are moved to client-side usePermissionConfig hooks. Additional improvements address React anti-patterns: lazy useState initializers replace mount-only useEffects, useDebounce replaces manual debounce state, optimistic UI is applied to the chunk toggle, and the mutate+new Promise wrapper is replaced with mutateAsync+try/catch. Skeleton mismatches and sidebar highlight bugs are also corrected.

  • Server-side auth/permission guards replaced with client-side usePermissionConfig + router.replace for knowledge, files, and tables; home and scheduled-tasks lose server-side membership guards without a client-side equivalent (acceptable because API routes still enforce auth)
  • ScheduleModal refactored to lazy useState initializers and key-based remount at the edit call-site
  • New useWorkspaceFileRecord hook shares the list cache query key, serving the file viewer from cache without an extra network request when the list is already loaded
  • isConnectorSyncingOrPending broadens polling to start immediately on connector creation, fixing documents not appearing live

Confidence Score: 5/5

Safe to merge — all remaining findings are P2 style/documentation suggestions with no correctness or data-integrity impact

No P0 or P1 defects found. The optimistic toggle rollback is correct, the mutateAsync refactor is clean, the useDebounce and lazy-useState replacements are idiomatic, and the connector polling fix correctly uses isConnectorSyncingOrPending. The three P2 comments cover a documentation gap, an accepted permission-flash trade-off, and a clarifying comment suggestion — none block merge.

apps/sim/hooks/queries/workspace-files.ts and apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx

Important Files Changed

Filename Overview
apps/sim/hooks/queries/workspace-files.ts Adds useWorkspaceFileRecord; fileId intentionally excluded from query key to share list cache — works correctly but cold-path fetches entire list
apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx Replaces 15-setter useEffect init with lazy useState + useMemo for cron state; correct because key-based remount is used at call site for edit modal
apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx Removes dead useEffect debounce and one-time URL init useEffect; replaces with useDebounce hook and lazy useState initializer; optimistic toggle with onError rollback added
apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx isConnectorSyncingOrPending replaces status==='syncing' check so polling activates on fresh connectors; mutate+Promise wrapper replaced with mutateAsync+try/catch
apps/sim/app/workspace/[workspaceId]/logs/logs.tsx Mount-only useEffect replaced with lazy useState initializer using window.location.search; SSR-safe via typeof window check
apps/sim/app/workspace/[workspaceId]/files/files.tsx Adds usePermissionConfig redirect replacing server-side guard; brief flash before config loads is an accepted trade-off of the SPA approach
apps/sim/app/workspace/[workspaceId]/files/[fileId]/view/file-viewer.tsx Refactored to self-fetch via useWorkspaceFileRecord; removes prop drilling and fetches file from list cache; renders null while loading

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["User navigates to /knowledge | /files | /tables"] --> B["page.tsx re-exports client component"]
    B --> C["Client component mounts"]
    C --> D["usePermissionConfig fetches async"]
    D --> E{Config loaded?}
    E -- "No (DEFAULT_CONFIG)" --> F["Render full page UI briefly"]
    E -- "Yes" --> G{Tab hidden?}
    G -- "Yes" --> H["router.replace to workspace home"]
    G -- "No" --> I["Render page normally"]
    F --> G

    J["Old server-side flow"] --> K["getSession + verifyWorkspaceMembership"]
    K --> L{Permitted?}
    L -- "No" --> M["Instant server redirect - no flash"]
    L -- "Yes" --> N["Render page"]
Loading

Comments Outside Diff (1)

  1. apps/sim/app/workspace/[workspaceId]/files/files.tsx, line 158-162 (link)

    P2 Restricted tab briefly visible before permission config loads

    usePermissionConfig fetches asynchronously. Until the response arrives, permissionData is undefined and the hook returns DEFAULT_PERMISSION_GROUP_CONFIG (all tabs allowed). The useEffect redirect only fires after config has loaded, so a user in a restricted permission group will momentarily see the full Files UI before being redirected.

    The same pattern appears in:

    • apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx (lines 503–507, hideKnowledgeBaseTab)
    • apps/sim/app/workspace/[workspaceId]/tables/tables.tsx (lines 892–897, hideTablesTab)

    The server-side approach prevented this flash entirely. If this is an acceptable trade-off, consider gating the main content behind isLoading from usePermissionConfig so nothing renders until access is confirmed.

Reviews (1): Last reviewed commit: "fix(kb): use active scope in useWorkspac..." | Re-trigger Greptile

Comment on lines +40 to +48
export function useWorkspaceFileRecord(workspaceId: string, fileId: string) {
return useQuery({
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,
})
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 fileId missing from query key — cache hit works, but cold-path fetches entire list

The hook intentionally shares the list(workspaceId, 'active') query key so that an already-cached file list is served without a second request. That works great for the warm path.

On a cold path (e.g. a user bookmarks /files/[fileId]/view and navigates directly), useWorkspaceFileRecord triggers a full fetchWorkspaceFiles call — fetching every file in the workspace just to find one. Because fileId is not in the query key, there is also no way for React Query to deduplicate or cache a per-file lookup separately from the list.

If this is the intended trade-off (list-or-nothing), a short comment on the hook documenting the cold-path behaviour would prevent future confusion.

Comment on lines +160 to +163
const initialCronState = useMemo(
() => (schedule ? parseCronToScheduleType(schedule.cronExpression) : null),
[schedule?.cronExpression]
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 State initialization relies on key-based remount at call-site

Every useState below reads schedule.* fields only at mount time — the values are never re-synchronized when schedule changes. This works correctly because scheduled-tasks.tsx provides a key prop on the edit modal that forces remount on each different task. If this component is reused elsewhere without a matching key, the displayed form values will silently stay stale. A short JSDoc or inline comment documenting this requirement would make the contract explicit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant