Skip to content

Commit 308b247

Browse files
committed
fix(nuqs): resolve 7 PR review findings on URL query-param state
- logs: clear the log-details `tab` param when the sidebar closes so a lingering `?tab=trace` no longer carries into the next opened log; deep-linked tabs still open on first mount. - logs dashboard: drive the in-memory workflow filtering off the same debounced search value the stats query uses (passed as a prop) so the chart and list stay consistent while typing. - knowledge document: make the URL `chunk` param the single source of truth for the open chunk (back/forward, deep links, and external navigation now drive the editor) instead of a one-time useState seed. - logs: drop the redundant `setUrlSearchQuery('')` after `resetFilters()` (resetFilters already clears `search`). - files: use a per-call `{ history: 'replace' }` override for the `shareFileId` share-modal open/close writes so toggling the modal does not pollute the back/forward stack; folder navigation keeps `push`. - tables + recently-deleted: trim search input before deriving the URL value so whitespace-only input no longer writes `?search=%20`.
1 parent 016b282 commit 308b247

7 files changed

Lines changed: 78 additions & 26 deletions

File tree

apps/sim/app/workspace/[workspaceId]/files/files.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,9 @@ export function Files() {
303303
const shareModal = shareFile ? (
304304
<ShareModal
305305
open
306-
onOpenChange={(open) => !open && setFilesParams({ shareFileId: null })}
306+
onOpenChange={(open) =>
307+
!open && setFilesParams({ shareFileId: null }, { history: 'replace' })
308+
}
307309
workspaceId={workspaceId}
308310
fileId={shareFile.id}
309311
fileName={shareFile.name}
@@ -995,7 +997,7 @@ export function Files() {
995997

996998
const handleShareSelected = useCallback(() => {
997999
const file = selectedFileRef.current
998-
if (file) setFilesParams({ shareFileId: file.id })
1000+
if (file) setFilesParams({ shareFileId: file.id }, { history: 'replace' })
9991001
}, [setFilesParams])
10001002

10011003
const handleBulkDelete = useCallback(() => {
@@ -1244,7 +1246,7 @@ export function Files() {
12441246

12451247
const handleContextMenuShare = useCallback(() => {
12461248
const item = contextMenuItemRef.current
1247-
if (item?.kind === 'file') setFilesParams({ shareFileId: item.file.id })
1249+
if (item?.kind === 'file') setFilesParams({ shareFileId: item.file.id }, { history: 'replace' })
12481250
closeContextMenu()
12491251
}, [closeContextMenu, setFilesParams])
12501252

apps/sim/app/workspace/[workspaceId]/files/search-params.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ const parseAsNewFlag = createParser<boolean>({
2020
* truth.
2121
*
2222
* - `folderId` is the currently open folder; it is shareable, bookmarkable, and
23-
* navigations between folders belong in the browser history (`history: 'push'`).
23+
* navigations between folders belong in the browser history (`history: 'push'`,
24+
* the group default).
2425
* - `new` marks a freshly-created file so the editor opens in compose mode; it is
2526
* read once on mount and stripped as the route stabilizes.
2627
* - `shareFileId` deep-links a file's share dialog open. The modal opens when the
27-
* id resolves to a loaded file; closing it clears the param. Opening it is a
28-
* destination, so (like folder navigation) it lands in the browser history.
28+
* id resolves to a loaded file; closing it clears the param. Opening and
29+
* closing the modal use a per-call `{ history: 'replace' }` override so the
30+
* dialog toggle does not pollute the back/forward stack (a deep link still
31+
* opens it on load).
2932
*/
3033
export const filesParsers = {
3134
folderId: parseAsString,
@@ -35,8 +38,10 @@ export const filesParsers = {
3538

3639
/**
3740
* Shared nuqs options for files query state. Folder navigation is a destination,
38-
* so it lands in the browser history; defaults clear from the URL to keep links
39-
* clean.
41+
* so the group default lands in the browser history; defaults clear from the URL
42+
* to keep links clean. Non-navigation writes (the `shareFileId` modal toggle)
43+
* pass a per-call `{ history: 'replace' }` override so they don't add back-stack
44+
* entries.
4045
*/
4146
export const filesUrlKeys = {
4247
history: 'push',

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,17 @@ export function Document({
188188

189189
const [selectedChunks, setSelectedChunks] = useState<Set<string>>(() => new Set())
190190

191-
// Inline editor state
192-
const [selectedChunkId, setSelectedChunkId] = useState<string | null>(() => chunkFromURL)
191+
// Inline editor state. The open chunk is sourced directly from the URL `chunk`
192+
// param (single source of truth) so back/forward, deep links, and external
193+
// navigation drive the editor; opening/closing a chunk writes the param.
194+
const selectedChunkId = chunkFromURL
195+
const setSelectedChunkId = useCallback(
196+
(chunkId: string | null) => {
197+
// Opening a chunk is a destination (back closes it); clearing replaces.
198+
void setDocumentParams({ chunk: chunkId }, chunkId !== null ? { history: 'push' } : undefined)
199+
},
200+
[setDocumentParams]
201+
)
193202
const [isCreatingNewChunk, setIsCreatingNewChunk] = useState(false)
194203
const [isDirty, setIsDirty] = useState(false)
195204
const [saveStatus, setSaveStatus] = useState<SaveStatus>('idle')
@@ -303,7 +312,7 @@ export function Document({
303312
setIsCreatingNewChunk(false)
304313
setIsDirty(false)
305314
setSaveStatus('idle')
306-
}, [])
315+
}, [setSelectedChunkId])
307316

308317
const guardDirtyAction = useCallback(
309318
(action: () => void) => {
@@ -420,7 +429,15 @@ export function Document({
420429
}
421430
}
422431
},
423-
[selectedChunk, currentChunkIndex, displayChunks, currentPage, totalPages, goToPage]
432+
[
433+
selectedChunk,
434+
currentChunkIndex,
435+
displayChunks,
436+
currentPage,
437+
totalPages,
438+
goToPage,
439+
setSelectedChunkId,
440+
]
424441
)
425442

426443
const handleNavigateChunk = useCallback(
@@ -444,7 +461,7 @@ export function Document({
444461

445462
const handleShowTags = useCallback(() => setShowTagsModal(true), [])
446463
const handleShowDeleteDoc = useCallback(() => setShowDeleteDocumentDialog(true), [])
447-
const handleClearSelectedChunk = useCallback(() => setSelectedChunkId(null), [])
464+
const handleClearSelectedChunk = useCallback(() => setSelectedChunkId(null), [setSelectedChunkId])
448465

449466
const breadcrumbs = useMemo<BreadcrumbItem[]>(
450467
() =>
@@ -516,7 +533,7 @@ export function Document({
516533
setIsDirty(false)
517534
setSaveStatus('idle')
518535
})
519-
}, [guardDirtyAction])
536+
}, [guardDirtyAction, setSelectedChunkId])
520537

521538
const handleChunkCreated = useCallback(
522539
async (chunkId: string) => {
@@ -549,7 +566,7 @@ export function Document({
549566
}
550567
setTimeout(checkAndSelect, 0)
551568
},
552-
[goToPage, totalPages]
569+
[goToPage, totalPages, setSelectedChunkId]
553570
)
554571

555572
const createAction = useMemo(
@@ -634,9 +651,12 @@ export function Document({
634651
[enabledFilter, goToPage]
635652
)
636653

637-
const handleChunkClick = useCallback((rowId: string) => {
638-
setSelectedChunkId(rowId)
639-
}, [])
654+
const handleChunkClick = useCallback(
655+
(rowId: string) => {
656+
setSelectedChunkId(rowId)
657+
},
658+
[setSelectedChunkId]
659+
)
640660

641661
const handleToggleEnabled = useCallback(
642662
(chunkId: string) => {

apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/dashboard.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ interface DashboardProps {
3838
stats?: DashboardStatsResponse
3939
isLoading: boolean
4040
error?: Error | null
41+
/**
42+
* Debounced search term. Comes pre-debounced from the parent (same value the
43+
* dashboard stats query uses) so the in-memory workflow list filtering and the
44+
* stats query stay in sync while typing.
45+
*/
46+
searchQuery: string
4147
}
4248

4349
/**
@@ -60,12 +66,12 @@ function toWorkflowExecution(wf: WorkflowStats): WorkflowExecution {
6066
}
6167
}
6268

63-
function DashboardInner({ stats, isLoading, error }: DashboardProps) {
69+
function DashboardInner({ stats, isLoading, error, searchQuery }: DashboardProps) {
6470
const [selectedSegments, setSelectedSegments] = useState<Record<string, number[]>>({})
6571
const [lastAnchorIndices, setLastAnchorIndices] = useState<Record<string, number>>({})
6672
const lastAnchorIndicesRef = useRef<Record<string, number>>({})
6773

68-
const { workflowIds, searchQuery, toggleWorkflowId, timeRange } = useLogFilters()
74+
const { workflowIds, toggleWorkflowId, timeRange } = useLogFilters()
6975

7076
const { workspaceId } = useParams<{ workspaceId: string }>()
7177
const { data: allWorkflowList = [], isPending: isWorkflowsPending } = useWorkflows(workspaceId)

apps/sim/app/workspace/[workspaceId]/logs/logs.tsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ import type {
5454
import { Resource } from '@/app/workspace/[workspaceId]/components'
5555
import { useLogFilters } from '@/app/workspace/[workspaceId]/logs/hooks/use-log-filters'
5656
import { useSearchState } from '@/app/workspace/[workspaceId]/logs/hooks/use-search-state'
57-
import { executionIdParam } from '@/app/workspace/[workspaceId]/logs/search-params'
57+
import {
58+
executionIdParam,
59+
logDetailsTabParam,
60+
logDetailsTabUrlKeys,
61+
} from '@/app/workspace/[workspaceId]/logs/search-params'
5862
import type { Suggestion } from '@/app/workspace/[workspaceId]/logs/types'
5963
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
6064
import { getBlock } from '@/blocks/registry'
@@ -233,6 +237,14 @@ export default function Logs() {
233237
const [executionId] = useQueryState(executionIdParam.key, executionIdParam.parser)
234238
const [pendingExecutionId, setPendingExecutionId] = useState<string | null>(() => executionId)
235239

240+
// The log-details `tab` param is owned/written by the details panel, but the
241+
// orchestrator must clear it when the panel closes so a lingering `?tab=trace`
242+
// never carries over to the next log opened from the list.
243+
const [, setLogDetailsTab] = useQueryState(logDetailsTabParam.key, {
244+
...logDetailsTabParam.parser,
245+
...logDetailsTabUrlKeys,
246+
})
247+
236248
// `urlSearchQuery` is the instant nuqs value (its URL write is debounced inside
237249
// `useLogFilters`); the query/filtering still debounce off it to avoid per-keystroke
238250
// fetches.
@@ -410,7 +422,10 @@ export default function Logs() {
410422
const handleCloseSidebar = useCallback(() => {
411423
dispatch({ type: 'CLOSE_SIDEBAR' })
412424
activeLogTabRef.current = 'overview'
413-
}, [])
425+
// Strip `tab` from the URL (back to the default) so reopening another log
426+
// starts on the overview tab instead of inheriting the closed log's tab.
427+
setLogDetailsTab(null)
428+
}, [setLogDetailsTab])
414429

415430
const handleActiveTabChange = useCallback((tab: string) => {
416431
activeLogTabRef.current = tab
@@ -459,9 +474,10 @@ export default function Logs() {
459474
}, [contextMenuLog, workflowIds, setWorkflowIds])
460475

461476
const handleClearAllFilters = useCallback(() => {
477+
// `resetFilters()` already clears `search` (sets it to null), so no separate
478+
// search reset is needed here.
462479
resetFilters()
463-
setUrlSearchQuery('')
464-
}, [resetFilters, setUrlSearchQuery])
480+
}, [resetFilters])
465481

466482
const handleOpenPreview = useCallback(() => {
467483
if (contextMenuLog?.id) {
@@ -1079,6 +1095,7 @@ export default function Logs() {
10791095
stats={dashboardStatsQuery.data}
10801096
isLoading={dashboardStatsQuery.isLoading}
10811097
error={dashboardStatsQuery.error}
1098+
searchQuery={debouncedSearchQuery}
10821099
/>
10831100
</div>
10841101
{sidebarOverlay}

apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,8 @@ export function RecentlyDeleted() {
162162
// it reads the instant value too.
163163
const setSearchTerm = useCallback(
164164
(value: string) => {
165-
const next = value.length > 0 ? value : null
165+
const trimmed = value.trim()
166+
const next = trimmed.length > 0 ? trimmed : null
166167
setRecentlyDeletedFilters(
167168
{ search: next },
168169
next === null ? undefined : { limitUrlUpdates: debounce(SEARCH_DEBOUNCE_MS) }

apps/sim/app/workspace/[workspaceId]/tables/tables.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ export function Tables() {
115115
// so it doesn't recompute on every keystroke.
116116
const setSearchTerm = useCallback(
117117
(value: string) => {
118-
const next = value.length > 0 ? value : null
118+
const trimmed = value.trim()
119+
const next = trimmed.length > 0 ? trimmed : null
119120
setTableFilters(
120121
{ search: next },
121122
next === null ? undefined : { limitUrlUpdates: debounce(SEARCH_DEBOUNCE_MS) }

0 commit comments

Comments
 (0)