From b91c69bc96f51edb030db051b520764f6b1c733b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 22 Jul 2026 10:39:05 -0700 Subject: [PATCH 1/8] improvement(url-state): migrate ee settings sections to nuqs deep-linkable view-state - audit-logs: types/time-range/start-date/end-date filters move to a co-located search-params.ts (reusing the logs kebab-token time-range parser); search binds to the shared settings ?search= via useSettingsSearch, replacing a hand-rolled debounce effect - access-control: group detail deep-links via ?group-id (push/replace-on-close); search via useSettingsSearch - custom-blocks: block detail deep-links via ?custom-block-id; create flow stays local; search via useSettingsSearch - data-drains + forks: search via useSettingsSearch; forks close now replaces history like the mcp reference pattern - polish: nullable-reason comment on logs startDate/endDate, stale debounce TSDoc now references useDebouncedSearchSetter --- .../(landing)/integrations/search-params.ts | 4 +- .../sim/app/(landing)/models/search-params.ts | 4 +- .../integrations/search-params.ts | 4 +- .../[workspaceId]/knowledge/search-params.ts | 4 +- .../[workspaceId]/logs/search-params.ts | 2 + .../settings/[section]/search-params.ts | 31 +++++++++++++ .../components/inbox/search-params.ts | 2 +- .../[workspaceId]/skills/search-params.ts | 3 +- .../components/access-control.tsx | 19 +++++--- .../ee/audit-logs/components/audit-logs.tsx | 43 +++++++------------ apps/sim/ee/audit-logs/search-params.ts | 35 +++++++++++++++ .../components/custom-blocks.tsx | 30 +++++++++---- .../components/data-drains-settings.tsx | 3 +- .../ee/workspace-forking/components/forks.tsx | 5 ++- 14 files changed, 135 insertions(+), 54 deletions(-) create mode 100644 apps/sim/ee/audit-logs/search-params.ts diff --git a/apps/sim/app/(landing)/integrations/search-params.ts b/apps/sim/app/(landing)/integrations/search-params.ts index 1672807299c..d93113b80d0 100644 --- a/apps/sim/app/(landing)/integrations/search-params.ts +++ b/apps/sim/app/(landing)/integrations/search-params.ts @@ -6,8 +6,8 @@ import { createSearchParamsCache, parseAsString } from 'nuqs/server' * so the filtered view is server-rendered for shareable, crawlable `?category=`/`?q=` * URLs — the same SSR pattern the blog index uses. * - * - `q` is the search filter; its URL write is debounced on the setter, never - * written per keystroke. + * - `q` is the search filter; its URL write is debounced via + * `useDebouncedSearchSetter`, never written per keystroke. * - `category` filters by integration type (`''` = all). */ export const integrationsParsers = { diff --git a/apps/sim/app/(landing)/models/search-params.ts b/apps/sim/app/(landing)/models/search-params.ts index 21c7ee09ec6..210556703ab 100644 --- a/apps/sim/app/(landing)/models/search-params.ts +++ b/apps/sim/app/(landing)/models/search-params.ts @@ -6,8 +6,8 @@ import { createSearchParamsCache, parseAsString } from 'nuqs/server' * so the filtered view is server-rendered for shareable, crawlable `?provider=`/`?q=` * URLs — the same SSR pattern the blog index uses. * - * - `q` is the search filter; its URL write is debounced on the setter, never - * written per keystroke. + * - `q` is the search filter; its URL write is debounced via + * `useDebouncedSearchSetter`, never written per keystroke. * - `provider` filters by provider id (`''` = all). */ export const modelsParsers = { diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/search-params.ts b/apps/sim/app/workspace/[workspaceId]/integrations/search-params.ts index f03533261f6..a5775bde969 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/search-params.ts +++ b/apps/sim/app/workspace/[workspaceId]/integrations/search-params.ts @@ -15,8 +15,8 @@ export const CONNECTED_LABEL = 'Connected' * pseudo-categories and are derived from the data set, so a plain string is * used; the `All` default clears from the URL. * - `search` is the integration search term. The input is controlled directly by - * the nuqs value; only its URL write is debounced via `limitUrlUpdates` - * (`debounce`) on the setter — never written on every keystroke. + * the nuqs value; only its URL write is debounced via + * `useDebouncedSearchSetter` — never written on every keystroke. */ export const integrationsParsers = { category: parseAsString.withDefault(ALL_CATEGORY), diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/search-params.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/search-params.ts index 8a10c0796d0..e27c2e3c8b3 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/search-params.ts +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/search-params.ts @@ -27,8 +27,8 @@ export const knowledgeSortParams = createSortParams(KNOWLEDGE_SORT_COLUMNS, { * * - `search` is the knowledge base name/description filter. The input is * controlled directly by the instant nuqs value; only its URL write is - * debounced via `limitUrlUpdates` (`debounce`) on the setter — never written - * on every keystroke. + * debounced via `useDebouncedSearchSetter` — never written on every + * keystroke. * - `connector` filters by connector presence; `content` filters by document * presence; `owner` filters by creator id. All are multi-select arrays. * diff --git a/apps/sim/app/workspace/[workspaceId]/logs/search-params.ts b/apps/sim/app/workspace/[workspaceId]/logs/search-params.ts index d2171915c21..935f9707c65 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/search-params.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/search-params.ts @@ -104,6 +104,8 @@ export const parseAsTriggers = createParser({ */ export const logFilterParsers = { timeRange: parseAsTimeRange.withDefault(DEFAULT_TIME_RANGE), + // Deliberately nullable: only populated when timeRange is "Custom range"; + // every preset range derives its window from the label instead. startDate: parseAsString, endDate: parseAsString, level: parseAsLogLevel.withDefault('all'), diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts b/apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts index bdf4eb6350c..10053058def 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts @@ -49,6 +49,37 @@ export const forkViewUrlKeys = { clearOnDefault: true, } as const +/** + * `group-id` deep-links the Access Control settings tab to a specific + * permission group's detail sub-view (mirrors `mcpServerId` on the MCP tab). + */ +export const groupIdParam = { + key: 'group-id', + parser: parseAsString, +} as const + +/** Opening a group's detail is a destination → push to history; clear on close. */ +export const groupIdUrlKeys = { + history: 'push', + clearOnDefault: true, +} as const + +/** + * `custom-block-id` deep-links the Custom Blocks settings tab to a specific + * block's detail sub-view. The "create new" flow stays in local state — only + * existing entities are deep-linkable. + */ +export const customBlockIdParam = { + key: 'custom-block-id', + parser: parseAsString, +} as const + +/** Opening a block's detail is a destination → push to history; clear on close. */ +export const customBlockIdUrlKeys = { + history: 'push', + clearOnDefault: true, +} as const + /** * `fork-direction` is the sync direction (push/pull) on the parent fork's detail * page — shareable view state, so a copied link opens the same side of the sync. diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/search-params.ts b/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/search-params.ts index f21a66f2d11..d1aa319c771 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/search-params.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/search-params.ts @@ -18,7 +18,7 @@ export type InboxStatusFilter = (typeof INBOX_STATUS_FILTERS)[number] * - `status` is the active status filter (feeds the tasks query key). * - `search` is the subject/sender/body name filter. The input is controlled * directly by the nuqs value; only its URL write is debounced via - * `limitUrlUpdates` (`debounce`) on the setter — never written per keystroke. + * `useDebouncedSearchSetter` — never written per keystroke. */ export const inboxTaskParsers = { status: parseAsStringLiteral(INBOX_STATUS_FILTERS).withDefault('all'), diff --git a/apps/sim/app/workspace/[workspaceId]/skills/search-params.ts b/apps/sim/app/workspace/[workspaceId]/skills/search-params.ts index ae63f33ebc4..554005fc76d 100644 --- a/apps/sim/app/workspace/[workspaceId]/skills/search-params.ts +++ b/apps/sim/app/workspace/[workspaceId]/skills/search-params.ts @@ -23,8 +23,7 @@ export const skillIdUrlKeys = { /** * `search` filters the skills list by name/description. The input is controlled * directly by the instant nuqs value; only its URL write is debounced via - * `limitUrlUpdates` (`debounce`) on the setter — never written on every - * keystroke. + * `useDebouncedSearchSetter` — never written on every keystroke. */ export const skillSearchParam = { key: 'search', diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index 11c3341d9cc..813b2d9df80 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -16,11 +16,17 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { ArrowRight, Plus } from 'lucide-react' import { useParams } from 'next/navigation' +import { useQueryState } from 'nuqs' import { isEnterprise } from '@/lib/billing/plan-helpers' import { getEnv, isTruthy } from '@/lib/core/config/env' +import { + groupIdParam, + groupIdUrlKeys, +} from '@/app/workspace/[workspaceId]/settings/[section]/search-params' import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' +import { useSettingsSearch } from '@/app/workspace/[workspaceId]/settings/components/use-settings-search' import { GroupDetail } from '@/ee/access-control/components/group-detail' import { WorkspaceSelect } from '@/ee/access-control/components/workspace-select' import { @@ -74,8 +80,11 @@ export function AccessControl({ isOrganizationAdmin, organizationId }: AccessCon const createPermissionGroup = useCreatePermissionGroup() - const [searchTerm, setSearchTerm] = useState('') - const [selectedGroupId, setSelectedGroupId] = useState(null) + const [searchTerm, setSearchTerm] = useSettingsSearch() + const [selectedGroupId, setSelectedGroupId] = useQueryState(groupIdParam.key, { + ...groupIdParam.parser, + ...groupIdUrlKeys, + }) const [showCreateModal, setShowCreateModal] = useState(false) const [newGroupName, setNewGroupName] = useState('') const [newGroupDescription, setNewGroupDescription] = useState('') @@ -160,8 +169,8 @@ export function AccessControl({ isOrganizationAdmin, organizationId }: AccessCon workspaceOptions={workspaceOptions} organizationWorkspaces={organizationWorkspaces} workspacesLoading={workspacesLoading} - onBack={() => setSelectedGroupId(null)} - onDeleted={() => setSelectedGroupId(null)} + onBack={() => void setSelectedGroupId(null, { history: 'replace' })} + onDeleted={() => void setSelectedGroupId(null, { history: 'replace' })} /> ) } @@ -198,7 +207,7 @@ export function AccessControl({ isOrganizationAdmin, organizationId }: AccessCon