Skip to content

Commit 5a85330

Browse files
committed
fix(resources): honor explicit default list params
1 parent b543cf9 commit 5a85330

5 files changed

Lines changed: 76 additions & 12 deletions

File tree

.claude/rules/sim-url-state.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,9 @@ Files, Tables, and Knowledge may persist their last-used filter/sort snapshot th
5353

5454
- nuqs remains authoritative while the module is open.
5555
- Zustand is consulted once on a clean module entry, after persisted state hydrates.
56-
- Effective URL filter/sort state wins as one complete snapshot and becomes the remembered value;
57-
omitted fields use URL defaults rather than merging with storage.
56+
- An explicit URL filter/sort parameter wins even when it resolves to the module default. The
57+
complete resolved URL snapshot becomes the remembered value; omitted fields use URL defaults
58+
rather than merging with storage.
5859
- Explicit filter/sort gestures commit the same complete snapshot to nuqs and Zustand together.
5960
- Never mirror subsequent URL changes with a synchronization effect or `popstate` listener.
6061
- Search and folder navigation remain URL-only and are excluded from the persisted snapshot.

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,13 @@ export const filesSortParams = createSortParams(FILE_SORT_COLUMNS, {
8383
direction: 'desc',
8484
})
8585

86+
const filesFilterUrlKeyMap = { uploadedBy: 'uploaded-by' } as const
87+
8688
export const filesListPreferenceConfig = {
8789
module: 'files',
8890
sortColumns: FILE_SORT_COLUMNS,
8991
filterKeys: ['type', 'size', 'uploadedBy'],
92+
preferenceUrlKeys: filesFilterUrlKeyMap,
9093
defaultPreference: {
9194
sort: filesSortParams.default,
9295
filters: { type: [], size: [], uploadedBy: [] },
@@ -98,5 +101,5 @@ export const filesFilterUrlKeys = {
98101
history: 'replace',
99102
shallow: true,
100103
clearOnDefault: true,
101-
urlKeys: { uploadedBy: 'uploaded-by' },
104+
urlKeys: filesFilterUrlKeyMap,
102105
} as const

apps/sim/hooks/use-resource-list-preferences.test.tsx

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* @vitest-environment jsdom
33
*/
44
import { act } from 'react'
5+
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
56
import { createRoot, type Root } from 'react-dom/client'
67
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
78
import { filesListPreferenceConfig } from '@/app/workspace/[workspaceId]/files/search-params'
@@ -30,12 +31,13 @@ interface HookProps {
3031

3132
const mountedRoots: Root[] = []
3233

33-
function renderPreferenceHook(props: HookProps) {
34+
function renderPreferenceHook(props: HookProps, searchParams = '') {
3435
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
3536
const root = createRoot(document.createElement('div'))
3637
mountedRoots.push(root)
3738
let result: ReturnType<typeof useResourceListPreferences>
3839
let currentProps = props
40+
let currentSearchParams = searchParams
3941

4042
function Probe() {
4143
result = useResourceListPreferences({
@@ -46,14 +48,21 @@ function renderPreferenceHook(props: HookProps) {
4648
return null
4749
}
4850

49-
act(() => root.render(<Probe />))
51+
const renderProbe = () => (
52+
<NuqsTestingAdapter hasMemory searchParams={currentSearchParams}>
53+
<Probe />
54+
</NuqsTestingAdapter>
55+
)
56+
57+
act(() => root.render(renderProbe()))
5058
return {
5159
get current() {
5260
return result
5361
},
54-
rerender(nextProps: HookProps) {
62+
rerender(nextProps: HookProps, nextSearchParams = currentSearchParams) {
5563
currentProps = nextProps
56-
act(() => root.render(<Probe />))
64+
currentSearchParams = nextSearchParams
65+
act(() => root.render(renderProbe()))
5766
},
5867
}
5968
}
@@ -112,6 +121,23 @@ describe('useResourceListPreferences', () => {
112121
expect(applyPreference).not.toHaveBeenCalled()
113122
})
114123

124+
it.each(['sort=updated&dir=desc', 'uploaded-by='])(
125+
'honors explicitly default-valued URL state instead of restoring a saved preference (%s)',
126+
async (searchParams) => {
127+
seedPreference(filteredPreference)
128+
const applyPreference = vi.fn()
129+
const result = renderPreferenceHook(
130+
{ preference: defaultPreference, applyPreference },
131+
searchParams
132+
)
133+
134+
expect(result.current.isReady).toBe(true)
135+
await flushEffects()
136+
expect(applyPreference).not.toHaveBeenCalled()
137+
expect(useResourceListPreferencesStore.getState().preferences).toEqual({})
138+
}
139+
)
140+
115141
it('starts restoring a saved preference once on a clean visit', async () => {
116142
seedPreference(filteredPreference)
117143
const applyPreference = vi.fn()
@@ -140,6 +166,23 @@ describe('useResourceListPreferences', () => {
140166
expect(applyPreference).toHaveBeenCalledOnce()
141167
})
142168

169+
it('lets an explicit URL change cancel a pending saved-preference restoration', async () => {
170+
seedPreference(filteredPreference)
171+
const applyPreference = vi.fn()
172+
const result = renderPreferenceHook({ preference: defaultPreference, applyPreference })
173+
174+
await flushEffects()
175+
expect(result.current.isReady).toBe(false)
176+
expect(applyPreference).toHaveBeenCalledWith(filteredPreference)
177+
178+
result.rerender({ preference: defaultPreference, applyPreference }, 'sort=updated&dir=desc')
179+
await flushEffects()
180+
181+
expect(result.current.isReady).toBe(true)
182+
expect(applyPreference).toHaveBeenCalledOnce()
183+
expect(useResourceListPreferencesStore.getState().preferences).toEqual({})
184+
})
185+
143186
it('stays unready while a clean entry waits for hydration', () => {
144187
vi.spyOn(useResourceListPreferencesStore.persist, 'rehydrate').mockImplementation(
145188
() => new Promise(() => undefined)
@@ -308,4 +351,8 @@ describe('module list preference configs', () => {
308351
filters: Object.fromEntries(keys.map((key) => [key, []])),
309352
})
310353
})
354+
355+
it('reuses the Files filter URL alias when detecting explicit preferences', () => {
356+
expect(filesListPreferenceConfig.preferenceUrlKeys).toEqual({ uploadedBy: 'uploaded-by' })
357+
})
311358
})

apps/sim/hooks/use-resource-list-preferences.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client'
22

33
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
4+
import { parseAsString, useQueryStates } from 'nuqs'
45
import type {
56
ResourceListPreference,
67
ResourceListPreferenceConfig,
@@ -86,6 +87,12 @@ export function useResourceListPreferences({
8687
)
8788
const setPreference = useResourceListPreferencesStore((state) => state.setPreference)
8889
const removePreference = useResourceListPreferencesStore((state) => state.removePreference)
90+
const preferenceQueryParsers = Object.fromEntries(
91+
['sort', 'dir', ...config.filterKeys].map((queryKey) => [queryKey, parseAsString])
92+
)
93+
const [preferenceQuery] = useQueryStates(preferenceQueryParsers, {
94+
urlKeys: config.preferenceUrlKeys,
95+
})
8996

9097
const defaultPreference = useMemo(
9198
() => normalizePreference(config.defaultPreference, config),
@@ -100,6 +107,8 @@ export function useResourceListPreferences({
100107
defaultPreference &&
101108
!resourceListPreferencesEqual(currentPreference, defaultPreference)
102109
)
110+
const hasExplicitUrlPreference = Object.values(preferenceQuery).some((value) => value !== null)
111+
const hasUrlPreference = hasExplicitUrlPreference || hasEffectiveUrlPreference
103112

104113
const rememberPreference = useCallback(
105114
(normalizedPreference: ResourceListPreference) => {
@@ -129,6 +138,10 @@ export function useResourceListPreferences({
129138
) {
130139
setPendingRestoration(null)
131140
setReadyKey(key)
141+
} else if (currentPreference && hasExplicitUrlPreference) {
142+
setPendingRestoration(null)
143+
rememberPreference(currentPreference)
144+
setReadyKey(key)
132145
}
133146
return
134147
}
@@ -142,7 +155,7 @@ export function useResourceListPreferences({
142155
return
143156
}
144157

145-
if (!resourceListPreferencesEqual(currentPreference, defaultPreference)) {
158+
if (hasUrlPreference) {
146159
rememberPreference(currentPreference)
147160
setReadyKey(key)
148161
return
@@ -163,7 +176,9 @@ export function useResourceListPreferences({
163176
currentPreference,
164177
defaultPreference,
165178
enabled,
179+
hasExplicitUrlPreference,
166180
hasHydrated,
181+
hasUrlPreference,
167182
key,
168183
pendingRestoration,
169184
rememberPreference,
@@ -215,10 +230,7 @@ export function useResourceListPreferences({
215230
}, [commitPreference, currentPreference, defaultPreference])
216231

217232
return {
218-
isReady:
219-
!enabled ||
220-
(pendingRestoration?.key !== key && hasEffectiveUrlPreference) ||
221-
readyKey === key,
233+
isReady: !enabled || (pendingRestoration?.key !== key && hasUrlPreference) || readyKey === key,
222234
commitPreference,
223235
setFilter,
224236
clearFilters,

apps/sim/stores/resource-list-preferences/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface ResourceListPreferenceConfig {
2121
module: ResourceListModule
2222
sortColumns: readonly string[]
2323
filterKeys: readonly string[]
24+
preferenceUrlKeys?: Readonly<Record<string, string>>
2425
defaultPreference: ResourceListPreference
2526
}
2627

0 commit comments

Comments
 (0)