Skip to content
This repository was archived by the owner on Jul 31, 2026. It is now read-only.
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
6 changes: 3 additions & 3 deletions src/components/WorkspaceSelector/OrgList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface OrgListProps {
orgs: Org[]
enableRecents: boolean
showRecents: boolean
selectedOrg: Org | null
selectedOrg?: Org
setSelectedOrg: (org: Org) => void
onSelectRecent: () => void
filterOrgFunc: (org: Org, search: string) => boolean
Expand Down Expand Up @@ -51,7 +51,7 @@ export function OrgList({
}, [selectedOrg, orgs])

return (
<div className="border-neutral-softest flex w-1/3 flex-col border-r">
<div className="border-neutral-softest flex h-full w-full flex-col @[640px]:w-1/3 @[640px]:border-r">
<SearchBox
inputRef={inputRef}
placeholder="Search organizations..."
Expand All @@ -69,7 +69,7 @@ export function OrgList({
onSelect={() => setSelectedOrg(org)}
value={`org-${org.slug}`}
className={cn(
'hover:!bg-accent/40 flex max-w-lg cursor-pointer flex-row gap-3 rounded-none p-4 text-base',
'hover:!bg-accent/40 flex cursor-pointer flex-row gap-3 rounded-none p-4 text-base @[640px]:max-w-lg',
!showRecents &&
selectedOrg?.slug === org.slug &&
'bg-accent/40 text-accent-foreground font-semibold'
Expand Down
2 changes: 1 addition & 1 deletion src/components/WorkspaceSelector/RecentWorkspaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface RecentWorkspacesProps {
orgsWithFilteredWorkspaces: Org[]
onSelect: (org: Org, workspace: Workspace) => void
fullWidth?: boolean
selectedOrg: Org | null
selectedOrg?: Org
selectedWorkspace: Workspace | null
handleCreateViewOpen: () => void
}
Expand Down
35 changes: 12 additions & 23 deletions src/components/WorkspaceSelector/WorkspaceList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { SearchBox } from './SearchBox'
import { Text } from '../Text'

interface WorkspaceListProps {
selectedOrg: Org
selectedOrg?: Org
selectedWorkspace: Workspace | null
handleCreateViewOpen: () => void
handleSelect: (org: Org, workspace: Workspace) => void
Expand All @@ -29,43 +29,35 @@ export function WorkspaceList({
const virtuoso = useRef<VirtuosoHandle | null>(null)
const [search, setSearch] = useState('')
const inputRef = useRef<HTMLInputElement>(null)
const [filteredWorkspaces, setFilteredWorkspaces] = useState<Workspace[]>(
selectedOrg?.workspaces || []
)

useEffect(() => {
setFilteredWorkspaces(
selectedOrg?.workspaces.filter((workspace) =>
filterWorkspaceFunc(workspace, search)
)
)
}, [search, selectedOrg?.workspaces, filterWorkspaceFunc])
const filteredWorkspaces =
selectedOrg?.workspaces.filter((workspace) =>
filterWorkspaceFunc(workspace, search)
) ?? []

useEffect(() => {
if (selectedWorkspace && virtuoso.current) {
const index = selectedOrg?.workspaces.findIndex(
if (selectedOrg && selectedWorkspace && virtuoso.current) {
const index = selectedOrg.workspaces.findIndex(
(workspace) => workspace.slug === selectedWorkspace.slug
)

// wait for the ref to update
setTimeout(() => {
virtuoso.current?.scrollToIndex({
index,
behavior: selectedOrg?.workspaces.length < 10 ? 'smooth' : 'auto',
behavior: 'smooth',
})
}, 100)
}
}, [selectedWorkspace, selectedOrg?.workspaces])
}, [selectedWorkspace, selectedOrg])

return (
<div className="flex w-2/3 flex-col">
<div className="flex h-full w-full flex-col @[640px]:w-2/3">
<SearchBox
inputRef={inputRef}
placeholder="Search workspaces..."
search={search}
setSearch={setSearch}
/>
{filteredWorkspaces.length === 0 ? (
{!selectedOrg || filteredWorkspaces.length === 0 ? (
<div className="flex flex-grow items-center justify-center">
<p className="text-body">
{search.length > 0
Expand All @@ -83,10 +75,7 @@ export function WorkspaceList({
workspace={workspace}
selectedOrg={selectedOrg}
handleSelect={handleSelect}
isSelected={
selectedOrg.id === selectedOrg.id &&
selectedWorkspace?.id === workspace.id
}
isSelected={selectedWorkspace?.id === workspace.id}
/>
)}
/>
Expand Down
35 changes: 31 additions & 4 deletions src/components/WorkspaceSelector/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,29 @@ const useWorkspaceSelectorState = (initialOrgs: Org[] = sampleData) => {
}
}

// Props type that preserves the discriminated union
type WorkspaceSelectorWithStateProps =
| ({
showCreateWorkspaceView: true
defaultSelectedOrg: Org
} & Partial<
Omit<
WorkspaceSelectorProps,
'showCreateWorkspaceView' | 'defaultSelectedOrg'
>
>)
| ({
showCreateWorkspaceView?: false
defaultSelectedOrg?: Org
} & Partial<
Omit<
WorkspaceSelectorProps,
'showCreateWorkspaceView' | 'defaultSelectedOrg'
>
>)

// Simplified component
const WorkspaceSelectorWithState = (props: Partial<WorkspaceSelectorProps>) => {
const WorkspaceSelectorWithState = (props: WorkspaceSelectorWithStateProps) => {
const state = useWorkspaceSelectorState(props.orgs)

return (
Expand Down Expand Up @@ -473,9 +494,9 @@ export const WithCreateWorkspaceViewShownByDefault: Story = {
export const WithSearchableOrgSelector: Story = {
...Default,
render: () => {
// Set seed once before generating all orgs for consistent data
faker.seed(42)
const orgs = Array.from({ length: 5000 }).map(() => {
// Set seed to ensure consistent data
faker.seed(42)
const slug = faker.lorem.slug({ min: 1, max: 3 })
return {
id: faker.string.uuid(),
Expand All @@ -486,7 +507,13 @@ export const WithSearchableOrgSelector: Story = {
updatedAt: new Date(),
}
})
return <WorkspaceSelectorWithState showCreateWorkspaceView orgs={orgs} />
return (
<WorkspaceSelectorWithState
showCreateWorkspaceView
orgs={orgs}
defaultSelectedOrg={orgs[0]}
/>
)
},
}

Expand Down
125 changes: 90 additions & 35 deletions src/components/WorkspaceSelector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Stack } from '../Stack'
import { CreateOrg } from './CreateOrg'
import { Heading } from '../Heading'
import { GlobalWorkspaceSelectorProps } from '@/types'
import { cn } from '@/lib/utils'

export interface Org {
id: string
Expand All @@ -30,7 +31,7 @@ export interface Workspace {
updatedAt: Date
}

export interface WorkspaceSelectorProps extends GlobalWorkspaceSelectorProps {
interface WorkspaceSelectorBaseProps extends GlobalWorkspaceSelectorProps {
onCreateOrg: (newOrgName: string) => Promise<Org>

/**
Expand All @@ -47,14 +48,22 @@ export interface WorkspaceSelectorProps extends GlobalWorkspaceSelectorProps {
*/
createTriggersSelection?: boolean

showCreateWorkspaceView?: boolean

defaultSelectedOrg?: Org

filterOrgFunc: (org: Org, search: string) => boolean
filterWorkspaceFunc: (workspace: Workspace, search: string) => boolean
}

export type WorkspaceSelectorProps = WorkspaceSelectorBaseProps &
(
| {
showCreateWorkspaceView: true
defaultSelectedOrg: Org
}
| {
showCreateWorkspaceView?: false
defaultSelectedOrg?: Org
}
)

const useViewTransition = () => {
const [isTransitioning, setIsTransitioning] = React.useState(false)

Expand Down Expand Up @@ -97,8 +106,8 @@ export function WorkspaceSelector({
}: WorkspaceSelectorProps) {
const [selectedWorkspace, setSelectedWorkspace] =
React.useState<Workspace | null>(null)
const [selectedOrg, setSelectedOrg] = React.useState<Org | null>(
defaultSelectedOrg ?? orgs[0]
const [selectedOrg, setSelectedOrg] = React.useState<Org | undefined>(
defaultSelectedOrg
)
const [createWorkspaceViewOpen, setCreateWorkspaceViewOpen] = React.useState(
showCreateWorkspaceView ?? false
Expand Down Expand Up @@ -166,7 +175,7 @@ export function WorkspaceSelector({
...prev,
workspaces: [...prev.workspaces, workspace],
}
: null
: undefined
)
setNewWorkspaceName('')
setCreateWorkspaceViewOpen(false)
Expand Down Expand Up @@ -225,10 +234,14 @@ export function WorkspaceSelector({
)
}, [startTransition])

const backToOrgSelector = React.useCallback(() => {
setSelectedOrg(undefined)
}, [])

return (
<div
ref={containerRef}
style={{ height }}
style={{ height, containerType: 'inline-size' }}
className="workspace-selector border-neutral-softest flex w-full flex-grow overflow-hidden rounded-md border"
>
{createOrgViewOpen ? (
Expand Down Expand Up @@ -261,7 +274,7 @@ export function WorkspaceSelector({
style={{
viewTransitionName: isTransitioning ? 'workspace-content' : '',
}}
className="flex w-full"
className="flex w-full flex-col @[640px]:flex-row"
>
<WorkspaceViewContents
orgs={orgs}
Expand All @@ -277,6 +290,7 @@ export function WorkspaceSelector({
handleSelectOrg={handleSelectOrg}
filterOrgFunc={filterOrgFunc}
filterWorkspaceFunc={filterWorkspaceFunc}
backToOrgSelector={backToOrgSelector}
/>
</div>
)}
Expand All @@ -286,7 +300,7 @@ export function WorkspaceSelector({

interface WorkspaceViewContentsProps {
orgs: Org[]
selectedOrg: Org | null
selectedOrg?: Org
selectedWorkspace: Workspace | null
handleSelect: (org: Org, workspace: Workspace, clearSearch: boolean) => void
showRecents: boolean
Expand All @@ -298,6 +312,7 @@ interface WorkspaceViewContentsProps {
handleSelectOrg: (org: Org) => void
filterOrgFunc: (org: Org, search: string) => boolean
filterWorkspaceFunc: (workspace: Workspace, search: string) => boolean
backToOrgSelector: () => void
}

function WorkspaceViewContents({
Expand All @@ -314,14 +329,16 @@ function WorkspaceViewContents({
handleSelectOrg,
filterOrgFunc,
filterWorkspaceFunc,
backToOrgSelector,
}: WorkspaceViewContentsProps) {
const showRecentsView = React.useMemo(
() => recents.length > 0 && showRecents,
[recents, showRecents]
)
return (
<>
<div className="bg-surface-primary border-neutral-softest flex h-full flex-1/3 flex-col items-center justify-center border-r">
{/* Wide container layout (>= 640px) - Left sidebar with title */}
<div className="bg-surface-primary border-neutral-softest hidden h-full flex-1/3 flex-col items-center justify-center border-r @[640px]:flex">
<div className="flex h-full max-w-80 flex-col items-center justify-center px-8 text-center">
<Stack align="center" gap={4}>
<div className="flex h-16 w-16 items-center justify-center">
Expand All @@ -339,8 +356,15 @@ function WorkspaceViewContents({
</div>
</div>

<div className="w-full flex-2/3">
<Command shouldFilter={false}>
{/* Narrow container header (< 640px) */}
<div className="border-neutral-softest flex w-full items-center gap-3 border-b px-4 py-3 @[640px]:hidden">
<Logo variant="icon" className="size-8" />
<Heading variant="xs">Select workspace</Heading>
</div>

{/* Main content area */}
<div className="flex h-full w-full flex-1 flex-col overflow-hidden @[640px]:flex-2/3">
<Command shouldFilter={false} className="h-full w-full">
{showRecentsView ? (
<div className="flex w-full flex-grow flex-row">
<OrgList
Expand All @@ -363,27 +387,58 @@ function WorkspaceViewContents({
/>
</div>
) : orgs.length > 0 ? (
<div className="flex h-full flex-row">
<OrgList
orgs={orgs}
selectedOrg={selectedOrg}
setSelectedOrg={handleSelectOrg}
onSelectRecent={() => setShowRecents(true)}
showRecents={showRecents}
enableRecents={recents.length > 0}
filterOrgFunc={(org, search) => filterOrgFunc(org, search)}
/>
<WorkspaceList
selectedOrg={selectedOrg!}
handleCreateViewOpen={handleCreateWorkspaceViewOpen}
handleSelect={(org, workspace) =>
handleSelect(org, workspace, false)
}
selectedWorkspace={selectedWorkspace}
filterWorkspaceFunc={(workspace, search) =>
filterWorkspaceFunc(workspace, search)
}
/>
<div className="flex h-full w-full flex-1 flex-col @[640px]:flex-row">
{/* Back button - narrow only, when org selected */}
<div
className={cn(
'border-neutral-softest flex-shrink-0 border-b p-3 @[640px]:hidden',
selectedOrg ? 'flex' : 'hidden'
)}
>
<button
onClick={backToOrgSelector}
className="text-body-sm text-link-primary hover:text-link-secondary flex items-center gap-2"
>
<span>←</span>
<span>Back to organizations</span>
</button>
</div>

{/* OrgList - always in wide, only when no org selected in narrow */}
<div
className={cn(
selectedOrg ? 'hidden @[640px]:contents' : 'contents'
)}
>
<OrgList
orgs={orgs}
selectedOrg={selectedOrg}
setSelectedOrg={handleSelectOrg}
onSelectRecent={() => setShowRecents(true)}
showRecents={showRecents}
enableRecents={recents.length > 0}
filterOrgFunc={(org, search) => filterOrgFunc(org, search)}
/>
</div>

{/* WorkspaceList - always in wide, only when org selected in narrow */}
<div
className={cn(
selectedOrg ? 'contents' : 'hidden @[640px]:contents'
)}
>
<WorkspaceList
selectedOrg={selectedOrg}
handleCreateViewOpen={handleCreateWorkspaceViewOpen}
handleSelect={(org, workspace) =>
handleSelect(org, workspace, false)
}
selectedWorkspace={selectedWorkspace}
filterWorkspaceFunc={(workspace, search) =>
filterWorkspaceFunc(workspace, search)
}
/>
</div>
</div>
) : (
<CommandEmpty
Expand Down