diff --git a/src/components/WorkspaceSelector/OrgList.tsx b/src/components/WorkspaceSelector/OrgList.tsx index d3f89144..1a04adca 100644 --- a/src/components/WorkspaceSelector/OrgList.tsx +++ b/src/components/WorkspaceSelector/OrgList.tsx @@ -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 @@ -51,7 +51,7 @@ export function OrgList({ }, [selectedOrg, orgs]) return ( -
+
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' diff --git a/src/components/WorkspaceSelector/RecentWorkspaces.tsx b/src/components/WorkspaceSelector/RecentWorkspaces.tsx index 238eca21..bb5b1c03 100644 --- a/src/components/WorkspaceSelector/RecentWorkspaces.tsx +++ b/src/components/WorkspaceSelector/RecentWorkspaces.tsx @@ -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 } diff --git a/src/components/WorkspaceSelector/WorkspaceList.tsx b/src/components/WorkspaceSelector/WorkspaceList.tsx index 3d2bd2bf..5a71ecb9 100644 --- a/src/components/WorkspaceSelector/WorkspaceList.tsx +++ b/src/components/WorkspaceSelector/WorkspaceList.tsx @@ -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 @@ -29,43 +29,35 @@ export function WorkspaceList({ const virtuoso = useRef(null) const [search, setSearch] = useState('') const inputRef = useRef(null) - const [filteredWorkspaces, setFilteredWorkspaces] = useState( - 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 ( -
+
- {filteredWorkspaces.length === 0 ? ( + {!selectedOrg || filteredWorkspaces.length === 0 ? (

{search.length > 0 @@ -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} /> )} /> diff --git a/src/components/WorkspaceSelector/index.stories.tsx b/src/components/WorkspaceSelector/index.stories.tsx index 39f7e47a..d87f7967 100644 --- a/src/components/WorkspaceSelector/index.stories.tsx +++ b/src/components/WorkspaceSelector/index.stories.tsx @@ -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) => { +const WorkspaceSelectorWithState = (props: WorkspaceSelectorWithStateProps) => { const state = useWorkspaceSelectorState(props.orgs) return ( @@ -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(), @@ -486,7 +507,13 @@ export const WithSearchableOrgSelector: Story = { updatedAt: new Date(), } }) - return + return ( + + ) }, } diff --git a/src/components/WorkspaceSelector/index.tsx b/src/components/WorkspaceSelector/index.tsx index 212bc50a..01e84753 100644 --- a/src/components/WorkspaceSelector/index.tsx +++ b/src/components/WorkspaceSelector/index.tsx @@ -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 @@ -30,7 +31,7 @@ export interface Workspace { updatedAt: Date } -export interface WorkspaceSelectorProps extends GlobalWorkspaceSelectorProps { +interface WorkspaceSelectorBaseProps extends GlobalWorkspaceSelectorProps { onCreateOrg: (newOrgName: string) => Promise /** @@ -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) @@ -97,8 +106,8 @@ export function WorkspaceSelector({ }: WorkspaceSelectorProps) { const [selectedWorkspace, setSelectedWorkspace] = React.useState(null) - const [selectedOrg, setSelectedOrg] = React.useState( - defaultSelectedOrg ?? orgs[0] + const [selectedOrg, setSelectedOrg] = React.useState( + defaultSelectedOrg ) const [createWorkspaceViewOpen, setCreateWorkspaceViewOpen] = React.useState( showCreateWorkspaceView ?? false @@ -166,7 +175,7 @@ export function WorkspaceSelector({ ...prev, workspaces: [...prev.workspaces, workspace], } - : null + : undefined ) setNewWorkspaceName('') setCreateWorkspaceViewOpen(false) @@ -225,10 +234,14 @@ export function WorkspaceSelector({ ) }, [startTransition]) + const backToOrgSelector = React.useCallback(() => { + setSelectedOrg(undefined) + }, []) + return (

{createOrgViewOpen ? ( @@ -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" >
)} @@ -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 @@ -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({ @@ -314,6 +329,7 @@ function WorkspaceViewContents({ handleSelectOrg, filterOrgFunc, filterWorkspaceFunc, + backToOrgSelector, }: WorkspaceViewContentsProps) { const showRecentsView = React.useMemo( () => recents.length > 0 && showRecents, @@ -321,7 +337,8 @@ function WorkspaceViewContents({ ) return ( <> -
+ {/* Wide container layout (>= 640px) - Left sidebar with title */} +
@@ -339,8 +356,15 @@ function WorkspaceViewContents({
-
- + {/* Narrow container header (< 640px) */} +
+ + Select workspace +
+ + {/* Main content area */} +
+ {showRecentsView ? (
) : orgs.length > 0 ? ( -
- setShowRecents(true)} - showRecents={showRecents} - enableRecents={recents.length > 0} - filterOrgFunc={(org, search) => filterOrgFunc(org, search)} - /> - - handleSelect(org, workspace, false) - } - selectedWorkspace={selectedWorkspace} - filterWorkspaceFunc={(workspace, search) => - filterWorkspaceFunc(workspace, search) - } - /> +
+ {/* Back button - narrow only, when org selected */} +
+ +
+ + {/* OrgList - always in wide, only when no org selected in narrow */} +
+ setShowRecents(true)} + showRecents={showRecents} + enableRecents={recents.length > 0} + filterOrgFunc={(org, search) => filterOrgFunc(org, search)} + /> +
+ + {/* WorkspaceList - always in wide, only when org selected in narrow */} +
+ + handleSelect(org, workspace, false) + } + selectedWorkspace={selectedWorkspace} + filterWorkspaceFunc={(workspace, search) => + filterWorkspaceFunc(workspace, search) + } + /> +
) : (