diff --git a/.claude/rules/sim-url-state.md b/.claude/rules/sim-url-state.md index 8a859e8a547..6790bc1b56d 100644 --- a/.claude/rules/sim-url-state.md +++ b/.claude/rules/sim-url-state.md @@ -46,6 +46,20 @@ These reads/mutations are **not** anti-patterns and stay as-is: - **Route navigations** — `router.push('/path/[id]?folderId=x')` that changes the route *path*, not just the current query. A nuqs setter only mutates the query on the current path; cross-path navigation stays on `router`. - **Read-once auth / redirect signals** — `token`, `callbackUrl`, `redirect`, `error`, `invite_flow`, `new` (invite signup flow), `upgraded`, `redirect_workflow`, etc. These are navigation signals consumed once (often read-then-strip), not synced view-state. Leave them on `useSearchParams`. Key names are per-surface: files' `new` is a genuine nuqs param (`files/search-params.ts`), while invite's `new` is a one-shot signup signal. +### Remembered list-preference exception + +Files, Tables, and Knowledge may persist their last-used filter/sort snapshot through +`useResourceListPreferences`. This is a fallback preference, not a second live source of truth: + +- nuqs remains authoritative while the module is open. +- Zustand is consulted once on a clean module entry, after persisted state hydrates. +- An explicit URL filter/sort parameter wins even when it resolves to the module default. The + complete resolved URL snapshot becomes the remembered value; omitted fields use URL defaults + rather than merging with storage. +- Explicit filter/sort gestures commit the same complete snapshot to nuqs and Zustand together. +- Never mirror subsequent URL changes with a synchronization effect or `popstate` listener. +- Search and folder navigation remain URL-only and are excluded from the persisted snapshot. + ## Per-feature `search-params.ts` — single source of truth Co-locate a `search-params.ts` next to the feature. Export the parser map (and shared options). Both the client (`useQueryStates`/`useQueryState`) and any server component (`createSearchParamsCache` from `nuqs/server`) import from this one file. Import parsers from `nuqs/server` so the module is safe to import in both client and server contexts. diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 73e9dd4710f..1288ad529ae 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -115,9 +115,11 @@ import { FileDocRoomProvider } from '@/app/workspace/[workspaceId]/files/compone import { FilesListContextMenu } from '@/app/workspace/[workspaceId]/files/components/files-list-context-menu' import { ShareModal } from '@/app/workspace/[workspaceId]/files/components/share-modal' import { useWorkspaceFilesRoom } from '@/app/workspace/[workspaceId]/files/hooks/use-workspace-files-room' +import FilesLoading from '@/app/workspace/[workspaceId]/files/loading' import { filesFilterParsers, filesFilterUrlKeys, + filesListPreferenceConfig, filesParsers, filesSortParams, filesUrlKeys, @@ -152,8 +154,10 @@ import { useContextMenu } from '@/hooks/use-context-menu' import { useDebouncedSearchSetter } from '@/hooks/use-debounced-search-setter' import { useInlineRename } from '@/hooks/use-inline-rename' import { usePermissionConfig } from '@/hooks/use-permission-config' +import { useResourceListPreferences } from '@/hooks/use-resource-list-preferences' import { useSearchFilterValue } from '@/hooks/use-search-filter-value' import { useUrlSort } from '@/hooks/use-url-sort' +import type { ResourceListPreference } from '@/stores/resource-list-preferences' type SaveStatus = 'idle' | 'saving' | 'saved' | 'error' type FileResourceItem = @@ -420,21 +424,58 @@ export function Files() { sort: sortColumn, dir: sortDirection, activeSort, - onSort, - onClear, + onSort: applyUrlSort, } = useUrlSort(filesSortParams, filesFilterUrlKeys) + const currentListPreference = useMemo( + () => ({ + sort: { column: sortColumn, direction: sortDirection }, + filters: { + type: typeFilter, + size: sizeFilter, + uploadedBy: uploadedByFilter, + }, + }), + [sortColumn, sortDirection, typeFilter, sizeFilter, uploadedByFilter] + ) + + const applyListPreference = useCallback( + (preference: ResourceListPreference) => { + void setFileFilters({ + type: [...preference.filters.type], + size: [...preference.filters.size], + uploadedBy: [...preference.filters.uploadedBy], + }) + applyUrlSort(preference.sort.column, preference.sort.direction) + }, + [applyUrlSort, setFileFilters] + ) + + const { + isReady: isListPreferenceReady, + setFilter: setListFilter, + clearFilters: clearFileFilters, + setSort: setListSort, + clearSort: clearListSort, + } = useResourceListPreferences({ + workspaceId, + config: filesListPreferenceConfig, + preference: currentListPreference, + applyPreference: applyListPreference, + enabled: fileIdFromRoute === null, + }) + const setTypeFilter = useCallback( - (next: string[]) => setFileFilters({ type: next }), - [setFileFilters] + (next: string[]) => setListFilter('type', next), + [setListFilter] ) const setSizeFilter = useCallback( - (next: string[]) => setFileFilters({ size: next }), - [setFileFilters] + (next: string[]) => setListFilter('size', next), + [setListFilter] ) const setUploadedByFilter = useCallback( - (next: string[]) => setFileFilters({ uploadedBy: next }), - [setFileFilters] + (next: string[]) => setListFilter('uploadedBy', next), + [setListFilter] ) const [creatingFile, setCreatingFile] = useState(false) @@ -1904,10 +1945,10 @@ export function Files() { { id: 'owner', label: 'Owner' }, ], active: activeSort, - onSort, - onClear, + onSort: setListSort, + onClear: clearListSort, }), - [activeSort, onSort, onClear] + [activeSort, setListSort, clearListSort] ) const hasActiveFilters = @@ -2004,11 +2045,7 @@ export function Files() { {hasActiveFilters && (