Skip to content

Commit cc49350

Browse files
committed
improvement(org): keep the rail's actions in its leaves and share its rows
Hooks that report mutation and update state move into the components that use them, and the command menu reads its own store, so neither re-renders the rail. Rail files import the design system's components directly, so the layout does not pull the whole barrel into every organization route. Rows and name dialogs share one implementation, and the resize edge uses literal sizes because the design tokens only resolve inside a surface.
1 parent ece9ec8 commit cc49350

27 files changed

Lines changed: 487 additions & 607 deletions

File tree

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

3-
import type { ComponentProps } from 'react'
43
import { OrganizationSettingsSidebar } from '@/app/o/[organizationId]/settings/organization-settings-sidebar'
54

5+
interface OrganizationSettingsRailProps {
6+
collapsed: boolean
7+
}
8+
69
/**
710
* Settings' own navigation inside the organization rail. It keeps the rail markup its chips read:
811
* collapsed labels hide through `.sidebar-container`, and their width through `group/rail`.
912
*/
10-
export function OrganizationSettingsRail(
11-
props: ComponentProps<typeof OrganizationSettingsSidebar>
12-
) {
13+
export function OrganizationSettingsRail({ collapsed }: OrganizationSettingsRailProps) {
1314
return (
1415
<div
1516
className='group/rail sidebar-container flex h-full flex-col overflow-hidden bg-[var(--surface-1)] [&_.group.cursor-pointer]:duration-0'
16-
data-collapsed={props.isCollapsed || undefined}
17+
data-collapsed={collapsed || undefined}
1718
>
18-
<OrganizationSettingsSidebar {...props} />
19+
<OrganizationSettingsSidebar isCollapsed={collapsed} showCollapsedTooltips={collapsed} />
1920
</div>
2021
)
2122
}

‎apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/sidebar-footer.tsx‎

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import { useRouter } from 'next/navigation'
2222
import { SlackIcon } from '@/components/icons'
2323
import { SettingsIntentLink } from '@/components/settings/settings-intent-link'
2424
import { signOutAndRedirect } from '@/lib/auth/sign-out'
25-
import { getDesktopUpdates } from '@/lib/desktop'
2625
import { rememberSettingsReturnUrl } from '@/lib/navigation/settings-return'
2726
import { getUserColor } from '@/lib/workspaces/colors'
2827
import { SidebarTooltip } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-tooltip'
@@ -34,6 +33,7 @@ import { useUserProfile } from '@/hooks/queries/user-profile'
3433
import {
3534
desktopUpdateActionLabel,
3635
hasAvailableDesktopUpdate,
36+
runDesktopUpdateAction,
3737
useDesktopUpdateState,
3838
} from '@/hooks/use-desktop-update-state'
3939
import { useSettingsDirtyStore } from '@/stores/settings/dirty/store'
@@ -120,14 +120,7 @@ export function SidebarFooter({
120120
const updateAvailable = hasAvailableDesktopUpdate(updateState)
121121
const canSignOut = Boolean(profile && profile.id !== ANONYMOUS_USER_ID)
122122

123-
const handleUpdateSelect = () => {
124-
const updates = getDesktopUpdates()
125-
if (updateState.status === 'ready') {
126-
updates?.install()
127-
} else if (updateState.status === 'available') {
128-
updates?.check()
129-
}
130-
}
123+
const handleUpdateSelect = () => runDesktopUpdateAction(updateState)
131124

132125
/**
133126
* Built from plain `img`/`div` rather than the emcn `Avatar`, whose Radix root

‎apps/sim/app/workspace/[workspaceId]/w/components/sidebar/utils.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function isSidebarBackgroundClick(event: MouseEvent<HTMLElement>): boolea
2020
event.target instanceof Element &&
2121
event.currentTarget.contains(event.target) &&
2222
!event.target.closest(
23-
'a, button, input, textarea, select, [role="button"], [contenteditable]:not([contenteditable="false"])'
23+
'a, button, input, textarea, select, [role="button"], [contenteditable]:not([contenteditable="false"]), [data-material="floating"]'
2424
)
2525
)
2626
}

‎apps/sim/hooks/queries/utils/prefetch-query-on-intent.ts‎

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ type QueryFilterKey = NonNullable<
44
NonNullable<Parameters<QueryClient['removeQueries']>[0]>['queryKey']
55
>
66

7+
/** Removes a speculative read that failed and that nothing is mounted to report. */
8+
export function dropFailedSpeculativeQuery(queryClient: QueryClient, queryKey: QueryKey): void {
9+
const state = queryClient.getQueryState(queryKey)
10+
if (state?.status !== 'error' || state.data !== undefined) return
11+
12+
queryClient.removeQueries({
13+
queryKey: queryKey as QueryFilterKey,
14+
exact: true,
15+
type: 'inactive',
16+
})
17+
}
18+
719
/**
820
* Starts a speculative query without allowing an inactive failure to poison a later mount.
921
* Mounted observers retain the error so their component can render truthful feedback.
@@ -12,14 +24,7 @@ export function prefetchQueryOnIntent<TQueryFnData, TError, TData, TQueryKey ext
1224
queryClient: QueryClient,
1325
options: FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>
1426
): void {
15-
void queryClient.prefetchQuery(options).then(() => {
16-
const state = queryClient.getQueryState(options.queryKey)
17-
if (state?.status !== 'error' || state.data !== undefined) return
18-
19-
queryClient.removeQueries({
20-
queryKey: options.queryKey as QueryFilterKey,
21-
exact: true,
22-
type: 'inactive',
23-
})
24-
})
27+
void queryClient
28+
.prefetchQuery(options)
29+
.then(() => dropFailedSpeculativeQuery(queryClient, options.queryKey))
2530
}

‎apps/sim/hooks/use-desktop-update-state.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ export function desktopUpdateActionLabel(state: DesktopUpdateState): string {
1717
return state.status === 'ready' ? 'Restart to update' : 'Update'
1818
}
1919

20+
/** Installs a downloaded update, or starts downloading an available one. */
21+
export function runDesktopUpdateAction(state: DesktopUpdateState): void {
22+
const updates = getDesktopUpdates()
23+
if (state.status === 'ready') updates?.install()
24+
else if (state.status === 'available') updates?.check()
25+
}
26+
2027
const INITIAL_UPDATE_STATE: DesktopUpdateState = { status: 'idle' }
2128

2229
export function useDesktopUpdateState(): DesktopUpdateState {

‎apps/sim/platform/app/(shell)/_components/command-menu/command-menu.tsx‎

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,34 @@
1+
'use client'
2+
13
import type {
24
ShellChat,
35
ShellDestination,
46
ShellWorkspace,
57
} from '@/platform/app/(shell)/_components/model'
68
import { CommandPalette } from '@/platform/design-system/components/command-palette/command-palette'
79
import { OverlayDialog } from '@/platform/design-system/components/modal/overlay-dialog'
10+
import { useSearchModalStore } from '@/stores/modals/search/store'
811

912
interface CommandMenuProps {
10-
open: boolean
11-
onClose: () => void
1213
navigate: (href: string) => void
1314
chats: readonly ShellChat[]
1415
workspaces: readonly ShellWorkspace[]
1516
destinations: readonly ShellDestination[]
1617
}
1718

18-
export function CommandMenu({
19-
open,
20-
onClose,
21-
navigate,
22-
chats,
23-
workspaces,
24-
destinations,
25-
}: CommandMenuProps) {
19+
/** The same store the desktop shell's View > Search toggles. Reads it here so the rail needn't. */
20+
export function CommandMenu({ navigate, chats, workspaces, destinations }: CommandMenuProps) {
21+
const open = useSearchModalStore((state) => state.isOpen)
22+
const setOpen = useSearchModalStore((state) => state.setOpen)
23+
if (!open) return null
2624
return (
27-
<OverlayDialog label='Search' presentation='command' open={open} onClose={onClose}>
25+
<OverlayDialog label='Search' presentation='command' open onClose={() => setOpen(false)}>
2826
<CommandPalette
2927
label='Search the platform'
30-
onSelect={navigate}
28+
onSelect={(href) => {
29+
setOpen(false)
30+
navigate(href)
31+
}}
3132
groups={[
3233
{ items: destinations },
3334
{

‎apps/sim/platform/app/(shell)/_components/invite-modal/invite-modal.tsx‎

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
'use client'
22

33
import { useState } from 'react'
4-
import { Modal, RecipientField, SelectField } from '@/platform/design-system/components'
4+
import { SelectField } from '@/platform/design-system/components/field/fields'
5+
import { Modal } from '@/platform/design-system/components/modal/modal'
6+
import { RecipientField } from '@/platform/design-system/components/workspace-fields/workspace-fields'
57
import { ORGANIZATION_ROLE_OPTIONS, useInviteMembers } from '@/platform/lib/data/invite'
68

79
interface InviteModalProps {
8-
open: boolean
9-
onOpenChange: (open: boolean) => void
10+
onClose: () => void
1011
}
1112

1213
interface Recipient {
@@ -21,18 +22,10 @@ type InviteRole = (typeof ORGANIZATION_ROLE_OPTIONS)[number]['value']
2122
* addresses stay in the field with their reasons so a retry resends only those, and the seat warning
2223
* never blocks, because the server decides per address.
2324
*/
24-
export function InviteModal({ open, onOpenChange }: InviteModalProps) {
25-
const invite = useInviteMembers(open)
25+
export function InviteModal({ onClose }: InviteModalProps) {
26+
const invite = useInviteMembers()
2627
const [recipients, setRecipients] = useState<Recipient[]>([])
2728
const [role, setRole] = useState<InviteRole>('member')
28-
const [wasOpen, setWasOpen] = useState(open)
29-
if (wasOpen !== open) {
30-
setWasOpen(open)
31-
if (open) {
32-
setRecipients([])
33-
setRole('member')
34-
}
35-
}
3629

3730
const problem = recipients
3831
.filter((recipient) => recipient.invalid)
@@ -42,10 +35,10 @@ export function InviteModal({ open, onOpenChange }: InviteModalProps) {
4235
return (
4336
<Modal
4437
title='Invite people'
45-
open={open}
46-
onOpenChange={onOpenChange}
38+
open
39+
onOpenChange={onClose}
4740
confirmLabel='Send invites'
48-
confirmDisabled={!invite.canInvite || recipients.length === 0 || Boolean(problem)}
41+
confirmDisabled={recipients.length === 0 || Boolean(problem)}
4942
onConfirm={async () => {
5043
const { failed, message } = await invite.submit(
5144
recipients.map(({ address }) => address),
@@ -65,14 +58,12 @@ export function InviteModal({ open, onOpenChange }: InviteModalProps) {
6558
validate={(address) => invite.addressProblem(address) === null}
6659
error={problem ?? undefined}
6760
hint={invite.seatWarning(recipients.length)}
68-
disabled={!invite.canInvite}
6961
/>
7062
<SelectField
7163
label='Role'
7264
options={ORGANIZATION_ROLE_OPTIONS}
7365
value={role}
7466
onValueChange={(next) => setRole(next === 'admin' ? 'admin' : 'member')}
75-
disabled={!invite.canInvite}
7667
/>
7768
</Modal>
7869
)

‎apps/sim/platform/app/(shell)/_components/model.ts‎

Lines changed: 6 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ export interface ShellWorkspace {
2727
export interface ShellChat extends ShellItem {
2828
/** Assistant chats live under Search, agent chats at the chat route. */
2929
href: string
30-
isActive?: boolean
31-
isUnread?: boolean
32-
isPinned?: boolean
30+
isActive: boolean
31+
isUnread: boolean
32+
isPinned: boolean
3333
}
3434

3535
export interface ShellDestination {
@@ -49,75 +49,17 @@ export interface ShellData {
4949
chats: ShellChat[]
5050
/** So a list can tell loading and failure from empty. */
5151
status: { workspaces: ShellQueryStatus; chats: ShellQueryStatus }
52-
workspaceCreation?: { canCreate: boolean; organizationId: string | null; reason: string | null }
52+
workspaceCreation: { canCreate: boolean; reason: string | null }
5353
/** False when the viewer's chat list would be refused, so the section is not offered. */
5454
chatsAvailable: boolean
5555
destinations: readonly ShellDestination[]
5656
}
5757

58-
/** A partial bulk failure still reports which chats went, so the view can narrow what is left. */
59-
interface ChatRemoval {
60-
removedIds: readonly string[]
61-
failure?: string
62-
}
63-
64-
export interface ShellChatActions {
65-
rename: (id: string, title: string) => Promise<void>
66-
remove: (ids: readonly string[]) => Promise<ChatRemoval>
67-
setPinned: (id: string, pinned: boolean) => void
68-
setRead: (id: string, read: boolean) => void
69-
retry: () => void
70-
}
71-
72-
export interface ShellWorkspaceActions {
73-
/** Resolves with where the new workspace opens. */
74-
create: (name: string) => Promise<string>
75-
rename: (id: string, name: string) => Promise<void>
76-
setPinned: (id: string, pinned: boolean) => void
77-
retry: () => void
78-
}
79-
80-
export interface ShellOrganizationActions {
81-
canEditIdentity: boolean
82-
canInvite: boolean
83-
logoAccept: string
84-
rename: (name: string) => Promise<void>
85-
uploadLogo: (file: File) => Promise<void>
86-
}
87-
88-
interface ShellSupportRequest {
89-
type: string
90-
subject: string
91-
message: string
92-
images: readonly File[]
93-
}
94-
95-
export interface ShellSupport {
96-
imageAccept: string
97-
requestTypes: readonly { value: string; label: string }[]
98-
/** Validates and compresses one image; rejects with why it cannot be attached. */
99-
prepareImage: (file: File) => Promise<File>
100-
submit: (request: ShellSupportRequest) => Promise<void>
101-
}
102-
103-
export interface ShellHelp {
104-
openDocumentation: () => void
105-
joinSlack: () => void
106-
support: ShellSupport
107-
changelogUrl?: string
108-
downloadUrl?: string
109-
desktopUpdate?: { label: string; disabled: boolean; run: () => void }
110-
}
111-
112-
/** What the shell's views can do, backed by the same mutations the workspace surface uses. */
58+
/** Leaving the current page. Each goes through the settings guard, so a draft is never lost. */
11359
export interface ShellActions {
114-
chats: ShellChatActions
115-
workspaces: ShellWorkspaceActions
116-
organization: ShellOrganizationActions
117-
help: ShellHelp
11860
signOut: () => void
11961
/** Runs `leave` now, or once the viewer agrees to discard unsaved settings. */
12062
requestLeave: (leave: () => void) => void
121-
/** Leaves through the settings guard, remembering where Settings' Back returns to. */
63+
/** Also remembers where Settings' Back returns to. */
12264
navigate: (href: string) => void
12365
}

0 commit comments

Comments
 (0)