Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .claude/rules/sim-url-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
96 changes: 77 additions & 19 deletions apps/sim/app/workspace/[workspaceId]/files/files.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -420,21 +424,58 @@ export function Files() {
sort: sortColumn,
dir: sortDirection,
activeSort,
onSort,
onClear,
onSort: applyUrlSort,
} = useUrlSort(filesSortParams, filesFilterUrlKeys)

const currentListPreference = useMemo<ResourceListPreference>(
() => ({
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,
Comment thread
j15z marked this conversation as resolved.
})

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)
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -2004,19 +2045,26 @@ export function Files() {
{hasActiveFilters && (
<Button
variant='ghost'
onClick={() => {
setTypeFilter([])
setSizeFilter([])
setUploadedByFilter([])
}}
onClick={clearFileFilters}
className='h-[32px] w-full text-caption hover-hover:bg-[var(--surface-active)]'
>
Clear all filters
</Button>
)}
</div>
)
}, [typeFilter, sizeFilter, uploadedByFilter, memberOptions, membersById, hasActiveFilters])
}, [
typeFilter,
sizeFilter,
uploadedByFilter,
memberOptions,
membersById,
hasActiveFilters,
setTypeFilter,
setSizeFilter,
setUploadedByFilter,
clearFileFilters,
])

/** Stable identity so the memoized `Resource.Options` can bail; an inline object cannot. */
const filterConfig = useMemo(() => ({ content: filterContent }), [filterContent])
Expand Down Expand Up @@ -2056,7 +2104,15 @@ export function Files() {
tags.push({ label, onRemove: () => setUploadedByFilter([]) })
}
return tags
}, [typeFilter, sizeFilter, uploadedByFilter, membersById])
}, [
typeFilter,
sizeFilter,
uploadedByFilter,
membersById,
setTypeFilter,
setSizeFilter,
setUploadedByFilter,
])

const listState = resourceListState({
rowCount: rows.length,
Expand All @@ -2071,9 +2127,11 @@ export function Files() {

const clearSearchAndFilters = () => {
setSearchTerm('')
void setFileFilters({ type: null, size: null, uploadedBy: null })
clearFileFilters()
}

if (!isListPreferenceReady) return <FilesLoading />

if (fileIdFromRoute && !selectedFile && isLoading) {
return (
<Resource>
Expand Down
16 changes: 15 additions & 1 deletion apps/sim/app/workspace/[workspaceId]/files/search-params.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createParser, parseAsArrayOf, parseAsString } from 'nuqs/server'
import { createSortParams } from '@/lib/url-state'
import type { ResourceListPreferenceConfig } from '@/stores/resource-list-preferences'

/** Sortable list columns, matching the `Resource.Options` sort menu. */
export const FILE_SORT_COLUMNS = ['name', 'size', 'type', 'created', 'owner', 'updated'] as const
Expand Down Expand Up @@ -82,10 +83,23 @@ export const filesSortParams = createSortParams(FILE_SORT_COLUMNS, {
direction: 'desc',
})

const filesFilterUrlKeyMap = { uploadedBy: 'uploaded-by' } as const

export const filesListPreferenceConfig = {
module: 'files',
sortColumns: FILE_SORT_COLUMNS,
filterKeys: ['type', 'size', 'uploadedBy'],
preferenceUrlKeys: filesFilterUrlKeyMap,
defaultPreference: {
sort: filesSortParams.default,
filters: { type: [], size: [], uploadedBy: [] },
},
} as const satisfies ResourceListPreferenceConfig

/** Filter/search/sort view-state: clean URLs, no back-stack churn. */
export const filesFilterUrlKeys = {
history: 'replace',
shallow: true,
clearOnDefault: true,
urlKeys: { uploadedBy: 'uploaded-by' },
urlKeys: filesFilterUrlKeyMap,
} as const
86 changes: 72 additions & 14 deletions apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ import {
KnowledgeBaseContextMenu,
KnowledgeListContextMenu,
} from '@/app/workspace/[workspaceId]/knowledge/components'
import KnowledgeLoading from '@/app/workspace/[workspaceId]/knowledge/loading'
import {
knowledgeListPreferenceConfig,
knowledgeParsers,
knowledgeSortParams,
knowledgeUrlKeys,
Expand All @@ -95,9 +97,11 @@ 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 { WorkflowFolder } from '@/stores/folders/types'
import type { ResourceListPreference } from '@/stores/resource-list-preferences'

const logger = createLogger('Knowledge')

Expand Down Expand Up @@ -280,21 +284,57 @@ export function Knowledge() {
sort: sortColumn,
dir: sortDirection,
activeSort,
onSort: onSortColumn,
onClear: onClearSort,
onSort: applyUrlSort,
} = useUrlSort(knowledgeSortParams, knowledgeUrlKeys)

const currentListPreference = useMemo<ResourceListPreference>(
() => ({
sort: { column: sortColumn, direction: sortDirection },
filters: {
connector: connectorFilter,
content: contentFilter,
owner: ownerFilter,
},
}),
[sortColumn, sortDirection, connectorFilter, contentFilter, ownerFilter]
)

const applyListPreference = useCallback(
(preference: ResourceListPreference) => {
void setKnowledgeFilters({
connector: [...preference.filters.connector],
content: [...preference.filters.content],
owner: [...preference.filters.owner],
})
applyUrlSort(preference.sort.column, preference.sort.direction)
},
[applyUrlSort, setKnowledgeFilters]
)

const {
isReady: isListPreferenceReady,
setFilter: setListFilter,
clearFilters: clearKnowledgeFilters,
setSort: setListSort,
clearSort: clearListSort,
} = useResourceListPreferences({
workspaceId,
config: knowledgeListPreferenceConfig,
preference: currentListPreference,
applyPreference: applyListPreference,
})

const setConnectorFilter = useCallback(
(next: string[]) => setKnowledgeFilters({ connector: next }),
[setKnowledgeFilters]
(next: string[]) => setListFilter('connector', next),
[setListFilter]
)
const setContentFilter = useCallback(
(next: string[]) => setKnowledgeFilters({ content: next }),
[setKnowledgeFilters]
(next: string[]) => setListFilter('content', next),
[setListFilter]
)
const setOwnerFilter = useCallback(
(next: string[]) => setKnowledgeFilters({ owner: next }),
[setKnowledgeFilters]
(next: string[]) => setListFilter('owner', next),
[setListFilter]
)

const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
Expand Down Expand Up @@ -1229,10 +1269,10 @@ export function Knowledge() {
{ id: 'updated', label: 'Last Updated' },
],
active: activeSort,
onSort: onSortColumn,
onClear: onClearSort,
onSort: setListSort,
onClear: clearListSort,
}),
[activeSort, onSortColumn, onClearSort]
[activeSort, setListSort, clearListSort]
)

const memberOptions: ChipDropdownOption[] = useMemo(
Expand Down Expand Up @@ -1319,7 +1359,15 @@ export function Knowledge() {
)}
</div>
),
[connectorFilter, contentFilter, ownerFilter, memberOptions]
[
connectorFilter,
contentFilter,
ownerFilter,
memberOptions,
setConnectorFilter,
setContentFilter,
setOwnerFilter,
]
)

/** Stable identity so the memoized `Resource.Options` can bail; an inline object cannot. */
Expand Down Expand Up @@ -1376,7 +1424,15 @@ export function Knowledge() {
tags.push({ label, onRemove: () => setOwnerFilter([]) })
}
return tags
}, [connectorFilter, contentFilter, ownerFilter, members])
}, [
connectorFilter,
contentFilter,
ownerFilter,
members,
setConnectorFilter,
setContentFilter,
setOwnerFilter,
])

const listState = resourceListState({
rowCount: rows.length,
Expand All @@ -1391,9 +1447,11 @@ export function Knowledge() {

const clearSearchAndFilters = () => {
setSearchQuery('')
void setKnowledgeFilters({ connector: null, content: null, owner: null })
clearKnowledgeFilters()
}

if (!isListPreferenceReady) return <KnowledgeLoading />

return (
<>
<Resource onContextMenu={handleContentContextMenu}>
Expand Down
11 changes: 11 additions & 0 deletions apps/sim/app/workspace/[workspaceId]/knowledge/search-params.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { parseAsArrayOf, parseAsString } from 'nuqs/server'
import { createSortParams } from '@/lib/url-state'
import type { ResourceListPreferenceConfig } from '@/stores/resource-list-preferences'

/** Sortable knowledge base columns, matching the `Resource.Options` sort menu. */
export const KNOWLEDGE_SORT_COLUMNS = [
Expand Down Expand Up @@ -43,6 +44,16 @@ export const knowledgeParsers = {
owner: parseAsArrayOf(parseAsString).withDefault([]),
} as const

export const knowledgeListPreferenceConfig = {
module: 'knowledge',
sortColumns: KNOWLEDGE_SORT_COLUMNS,
filterKeys: ['connector', 'content', 'owner'],
defaultPreference: {
sort: knowledgeSortParams.default,
filters: { connector: [], content: [], owner: [] },
},
} as const satisfies ResourceListPreferenceConfig

/** Filter/search/sort view-state: clean URLs, no back-stack churn. */
export const knowledgeUrlKeys = {
history: 'replace',
Expand Down
Loading
Loading