Skip to content

Commit b8e88b1

Browse files
authored
improvement(settings): react health pass across settings surface (#5324)
- workflow-mcp: move tool-edit state seeding from a derive effect into the Edit event handler - admin: drop 2 inert useMemo (page math) and 1 unused useCallback - inbox + teammates: migrate filter/search view-state from useState to nuqs URL params (shareable, debounced writes) - team roster: memoize O(workspaces x members) grouping so keystroke search stays cheap at scale - remove dead code (unused props/locals: currentUserEmail, setOrgName, isLoadingWorkflows, isTeam)
1 parent 0575875 commit b8e88b1

11 files changed

Lines changed: 158 additions & 76 deletions

File tree

apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx

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

3-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
3+
import { useEffect, useMemo, useRef, useState } from 'react'
44
import { Badge, Button, ChipInput, ChipSelect, cn, Label, Search, Switch } from '@sim/emcn'
55
import { getErrorMessage } from '@sim/utils/errors'
66
import { useParams } from 'next/navigation'
@@ -77,29 +77,23 @@ export function Admin() {
7777
setSearchInput((current) => (current === searchQuery ? current : searchQuery))
7878
}, [searchQuery])
7979

80-
const totalPages = useMemo(
81-
() => Math.ceil((usersData?.total ?? 0) / PAGE_SIZE),
82-
[usersData?.total]
83-
)
84-
const currentPage = useMemo(() => Math.floor(usersOffset / PAGE_SIZE) + 1, [usersOffset])
80+
const totalPages = Math.ceil((usersData?.total ?? 0) / PAGE_SIZE)
81+
const currentPage = Math.floor(usersOffset / PAGE_SIZE) + 1
8582

8683
const handleSuperUserModeToggle = async (checked: boolean) => {
8784
if (checked !== settings?.superUserModeEnabled && !updateSetting.isPending) {
8885
await updateSetting.mutateAsync({ key: 'superUserModeEnabled', value: checked })
8986
}
9087
}
9188

92-
const handleMothershipEnvironmentChange = useCallback(
93-
async (nextEnvironment: MothershipEnvironment) => {
94-
if (nextEnvironment !== settings?.mothershipEnvironment && !updateSetting.isPending) {
95-
await updateSetting.mutateAsync({
96-
key: 'mothershipEnvironment',
97-
value: nextEnvironment,
98-
})
99-
}
100-
},
101-
[settings?.mothershipEnvironment, updateSetting]
102-
)
89+
const handleMothershipEnvironmentChange = async (nextEnvironment: MothershipEnvironment) => {
90+
if (nextEnvironment !== settings?.mothershipEnvironment && !updateSetting.isPending) {
91+
await updateSetting.mutateAsync({
92+
key: 'mothershipEnvironment',
93+
value: nextEnvironment,
94+
})
95+
}
96+
}
10397

10498
const handleImport = () => {
10599
if (!workflowId.trim()) return

apps/sim/app/workspace/[workspaceId]/settings/components/billing/subscription-permissions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function getSubscriptionPermissions(
3636
subscription: SubscriptionState,
3737
userRole: UserRole
3838
): SubscriptionPermissions {
39-
const { isFree, isPro, isTeam, isEnterprise, isPaid, isOrgScoped } = subscription
39+
const { isFree, isPro, isEnterprise, isPaid, isOrgScoped } = subscription
4040
const { isTeamAdmin } = userRole
4141

4242
// Non-admin org members see the "team member" view: no edit / no cancel

apps/sim/app/workspace/[workspaceId]/settings/components/inbox/components/inbox-task-list/inbox-task-list.tsx

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
'use client'
22

3-
import { useCallback, useMemo, useState } from 'react'
3+
import { useCallback, useMemo } from 'react'
44
import { Badge, ChipInput, ChipSelect, Search } from '@sim/emcn'
55
import { formatRelativeTime } from '@sim/utils/formatting'
66
import { ArrowRight, Paperclip } from 'lucide-react'
77
import { useParams, useRouter } from 'next/navigation'
8+
import { debounce, useQueryStates } from 'nuqs'
9+
import {
10+
type InboxStatusFilter,
11+
inboxTaskParsers,
12+
inboxTaskUrlKeys,
13+
} from '@/app/workspace/[workspaceId]/settings/components/inbox/search-params'
814
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
915
import type { InboxTaskItem } from '@/hooks/queries/inbox'
1016
import { useInboxConfig, useInboxTasks } from '@/hooks/queries/inbox'
@@ -18,7 +24,10 @@ const STATUS_OPTIONS = [
1824
{ value: 'rejected', label: 'Rejected' },
1925
] as const
2026

21-
type StatusFilter = (typeof STATUS_OPTIONS)[number]['value']
27+
type StatusFilter = InboxStatusFilter
28+
29+
/** Debounce window for `search` URL writes; the input itself stays instant. */
30+
const SEARCH_DEBOUNCE_MS = 300 as const
2231

2332
const STATUS_BADGES: Record<
2433
string,
@@ -36,8 +45,26 @@ export function InboxTaskList() {
3645
const router = useRouter()
3746
const workspaceId = params.workspaceId as string
3847

39-
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all')
40-
const [searchTerm, setSearchTerm] = useState('')
48+
const [{ status: statusFilter, search: searchTerm }, setInboxFilters] = useQueryStates(
49+
inboxTaskParsers,
50+
inboxTaskUrlKeys
51+
)
52+
53+
/**
54+
* The input is controlled directly by the instant nuqs value; only the URL
55+
* write is debounced. Filtering below is cheap in-memory over the loaded
56+
* tasks, so it reads the instant value too.
57+
*/
58+
const setSearchTerm = useCallback(
59+
(value: string) => {
60+
const next = value.length > 0 ? value : null
61+
setInboxFilters(
62+
{ search: next },
63+
next === null ? undefined : { limitUrlUpdates: debounce(SEARCH_DEBOUNCE_MS) }
64+
)
65+
},
66+
[setInboxFilters]
67+
)
4168

4269
const { data: config } = useInboxConfig(workspaceId)
4370
const { data: tasksData, isLoading } = useInboxTasks(workspaceId, {
@@ -80,7 +107,7 @@ export function InboxTaskList() {
80107
value={statusFilter}
81108
onChange={(value) => {
82109
if (STATUS_OPTIONS.some((option) => option.value === value)) {
83-
setStatusFilter(value as StatusFilter)
110+
setInboxFilters({ status: value as StatusFilter })
84111
}
85112
}}
86113
options={STATUS_OPTIONS.map((opt) => ({ label: opt.label, value: opt.value }))}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { parseAsString, parseAsStringLiteral } from 'nuqs/server'
2+
3+
/** Selectable status filters for the inbox task list. */
4+
export const INBOX_STATUS_FILTERS = [
5+
'all',
6+
'completed',
7+
'processing',
8+
'received',
9+
'failed',
10+
'rejected',
11+
] as const
12+
13+
export type InboxStatusFilter = (typeof INBOX_STATUS_FILTERS)[number]
14+
15+
/**
16+
* Co-located, typed URL query-param definitions for the inbox task list.
17+
*
18+
* - `status` is the active status filter (feeds the tasks query key).
19+
* - `search` is the subject/sender/body name filter. The input is controlled
20+
* directly by the nuqs value; only its URL write is debounced via
21+
* `limitUrlUpdates` (`debounce`) on the setter — never written per keystroke.
22+
*/
23+
export const inboxTaskParsers = {
24+
status: parseAsStringLiteral(INBOX_STATUS_FILTERS).withDefault('all'),
25+
search: parseAsString.withDefault(''),
26+
} as const
27+
28+
/** Status/search view-state: clean URLs, no back-stack churn. */
29+
export const inboxTaskUrlKeys = {
30+
history: 'replace',
31+
clearOnDefault: true,
32+
} as const

apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/no-organization-view/no-organization-view.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ interface NoOrganizationViewProps {
1515
hasTeamPlan: boolean
1616
hasEnterprisePlan: boolean
1717
orgName: string
18-
setOrgName: (name: string) => void
1918
orgSlug: string
2019
setOrgSlug: (slug: string) => void
2120
onOrgNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void
@@ -30,7 +29,6 @@ export function NoOrganizationView({
3029
hasTeamPlan,
3130
hasEnterprisePlan,
3231
orgName,
33-
setOrgName,
3432
orgSlug,
3533
setOrgSlug,
3634
onOrgNameChange,

apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ interface OrganizationMemberListsProps {
7171
roster: OrganizationRoster | null | undefined
7272
isLoadingRoster: boolean
7373
currentUserId: string
74-
currentUserEmail: string
7574
onRemoveMember: (member: Member) => void
7675
onTransferOwnership?: () => void
7776
}
@@ -87,7 +86,6 @@ export function OrganizationMemberLists({
8786
roster,
8887
isLoadingRoster,
8988
currentUserId,
90-
currentUserEmail,
9189
onRemoveMember,
9290
onTransferOwnership,
9391
}: OrganizationMemberListsProps) {
@@ -376,6 +374,38 @@ export function OrganizationMemberLists({
376374
const hasOrgMatches = filteredOrgMembers.length + filteredOrgPending.length > 0
377375
const showMembersSection = !isActiveSearch || hasOrgMatches
378376

377+
/**
378+
* Group each workspace's members and pending invites once per roster change.
379+
* This is O(workspaces × members) and independent of the search query, so
380+
* hoisting it out of render keeps keystroke filtering cheap on large orgs.
381+
*/
382+
const workspaceGroups = useMemo(
383+
() =>
384+
workspaces.map((workspace) => {
385+
const workspaceMembers = members
386+
.map((member) => ({
387+
member,
388+
access: member.workspaces.find((w) => w.workspaceId === workspace.id),
389+
}))
390+
.filter((entry): entry is { member: RosterMember; access: RosterWorkspaceAccess } =>
391+
Boolean(entry.access)
392+
)
393+
const workspaceInvites = pendingInvitations
394+
.map((invitation) => ({
395+
invitation,
396+
access: invitation.workspaces.find((w) => w.workspaceId === workspace.id),
397+
}))
398+
.filter(
399+
(
400+
entry
401+
): entry is { invitation: RosterPendingInvitation; access: RosterWorkspaceAccess } =>
402+
Boolean(entry.access)
403+
)
404+
return { workspace, workspaceMembers, workspaceInvites }
405+
}),
406+
[workspaces, members, pendingInvitations]
407+
)
408+
379409
return (
380410
<>
381411
<div className='flex items-center gap-2'>
@@ -399,27 +429,7 @@ export function OrganizationMemberLists({
399429
</MemberSection>
400430
)}
401431

402-
{workspaces.map((workspace) => {
403-
const workspaceMembers = members
404-
.map((member) => ({
405-
member,
406-
access: member.workspaces.find((w) => w.workspaceId === workspace.id),
407-
}))
408-
.filter((entry): entry is { member: RosterMember; access: RosterWorkspaceAccess } =>
409-
Boolean(entry.access)
410-
)
411-
const workspaceInvites = pendingInvitations
412-
.map((invitation) => ({
413-
invitation,
414-
access: invitation.workspaces.find((w) => w.workspaceId === workspace.id),
415-
}))
416-
.filter(
417-
(
418-
entry
419-
): entry is { invitation: RosterPendingInvitation; access: RosterWorkspaceAccess } =>
420-
Boolean(entry.access)
421-
)
422-
432+
{workspaceGroups.map(({ workspace, workspaceMembers, workspaceInvites }) => {
423433
const visibleMembers = workspaceMembers.filter(({ member }) =>
424434
matches(member.name, member.email)
425435
)

apps/sim/app/workspace/[workspaceId]/settings/components/team-management/team-management.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,6 @@ export function TeamManagement() {
293293
hasTeamPlan={hasTeamPlan}
294294
hasEnterprisePlan={hasEnterprisePlan}
295295
orgName={orgName}
296-
setOrgName={setOrgName}
297296
orgSlug={orgSlug}
298297
setOrgSlug={setOrgSlug}
299298
onOrgNameChange={handleOrgNameChange}
@@ -337,7 +336,6 @@ export function TeamManagement() {
337336
roster={roster ?? null}
338337
isLoadingRoster={isLoadingRoster}
339338
currentUserId={session?.user?.id ?? ''}
340-
currentUserEmail={session?.user?.email ?? ''}
341339
onRemoveMember={handleRemoveMember}
342340
onTransferOwnership={handleOpenTransferDialog}
343341
/>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { parseAsString } from 'nuqs/server'
2+
3+
/**
4+
* Co-located, typed URL query-param definition for the Teammates view.
5+
*
6+
* `search` is the name/email filter. The input is controlled directly by the
7+
* nuqs value; only its URL write is debounced via `limitUrlUpdates`.
8+
*/
9+
export const teammatesSearchParam = {
10+
key: 'search',
11+
parser: parseAsString.withDefault(''),
12+
} as const
13+
14+
/** Search view-state: clean URLs, no back-stack churn. */
15+
export const teammatesUrlKeys = {
16+
history: 'replace',
17+
clearOnDefault: true,
18+
} as const

apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ChipDropdown, Plus, toast } from '@sim/emcn'
55
import { getErrorMessage } from '@sim/utils/errors'
66
import { useQueryClient } from '@tanstack/react-query'
77
import { useParams, useRouter } from 'next/navigation'
8+
import { debounce, useQueryState } from 'nuqs'
89
import {
910
RoleLockTooltip,
1011
type WorkspaceRoleSource,
@@ -18,6 +19,10 @@ import {
1819
} from '@/app/workspace/[workspaceId]/settings/components/member-list'
1920
import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu'
2021
import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
22+
import {
23+
teammatesSearchParam,
24+
teammatesUrlKeys,
25+
} from '@/app/workspace/[workspaceId]/settings/components/teammates/search-params'
2126
import { isBillingEnabled } from '@/app/workspace/[workspaceId]/settings/navigation'
2227
import { InviteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal'
2328
import {
@@ -71,7 +76,11 @@ export function Teammates() {
7176
const params = useParams()
7277
const workspaceId = (params?.workspaceId as string) || ''
7378

74-
const [searchTerm, setSearchTerm] = useState('')
79+
const [searchTerm, setSearchTerm] = useQueryState(teammatesSearchParam.key, {
80+
...teammatesSearchParam.parser,
81+
...teammatesUrlKeys,
82+
limitUrlUpdates: debounce(300),
83+
})
7584
const [isInviteModalOpen, setIsInviteModalOpen] = useState(false)
7685

7786
const { data: permissions, isPending: permissionsLoading } =
@@ -167,7 +176,7 @@ export function Teammates() {
167176
<SettingsPanel
168177
search={{
169178
value: searchTerm,
170-
onChange: setSearchTerm,
179+
onChange: (value) => void setSearchTerm(value),
171180
placeholder: 'Search teammates...',
172181
}}
173182
actions={[

apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/components/create-workflow-mcp-server-modal/create-workflow-mcp-server-modal.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,13 @@ interface CreateWorkflowMcpServerModalProps {
2929
onOpenChange: (open: boolean) => void
3030
workspaceId: string
3131
workflowOptions?: ComboboxOption[]
32-
isLoadingWorkflows?: boolean
3332
}
3433

3534
export function CreateWorkflowMcpServerModal({
3635
open,
3736
onOpenChange,
3837
workspaceId,
3938
workflowOptions,
40-
isLoadingWorkflows = false,
4139
}: CreateWorkflowMcpServerModalProps) {
4240
const createServerMutation = useCreateWorkflowMcpServer()
4341

0 commit comments

Comments
 (0)