feat: studio mobile nav (#31080)
* SheetProvider in studio * mobile global nav * view sub navigation * fix ProjectLayout conditional * Account mobile layout * width * hide * layout * overflow * responsive button * overflow * fix mobile nav if IS_PLATFORM * refactor * hide if no content * show command menu item on mobile * ui-patterns sheet provider * use client * show if productMenu * show if productMenu * drag to close * drag to close * 16px input fontsize on inputs to avoid zoom on mobile * offscreen test element * offscreen test element * offscreen test element * update test * update test * update test * pb * test? * test * test * test * spec-click-target * remove console * remove unused import * add dependency to sheet content * close sheet on router.asPath change * remove provider and use simple Sheet * update sidebar mobile * align labels * avoid input zoom on mobile * remove yo
This commit is contained in:
committed by
GitHub
parent
70460772f1
commit
0035920bf1
@@ -43,7 +43,7 @@ const HomePageActions = ({
|
||||
const { isSuccess: orgsLoaded } = useOrganizationsQuery()
|
||||
|
||||
return (
|
||||
<div className="flex gap-x-3">
|
||||
<div className="flex flex-col gap-2 md:gap-3 md:flex-row">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="primary">
|
||||
@@ -72,12 +72,12 @@ const HomePageActions = ({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
size="tiny"
|
||||
placeholder="Search for a project"
|
||||
icon={<Search size={16} />}
|
||||
className="w-64 [&>div>div>div>input]:!pl-7 [&>div>div>div>div]:!pl-2"
|
||||
className="w-full flex-1 md:w-64 [&>div>div>div>input]:!pl-7 [&>div>div>div>div]:!pl-2"
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
/>
|
||||
|
||||
@@ -57,7 +57,7 @@ const SQLQuickstarts = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="block h-full space-y-8 overflow-y-auto p-6">
|
||||
<div className="block h-full space-y-8 overflow-y-auto p-4 md:p-6">
|
||||
<div className="mb-8">
|
||||
<div className="mb-4">
|
||||
<h1 className="text-foreground mb-3 text-xl">Quickstarts</h1>
|
||||
|
||||
@@ -57,7 +57,7 @@ const SQLTemplates = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="block h-full space-y-8 overflow-y-auto p-6">
|
||||
<div className="block h-full space-y-8 overflow-y-auto p-4 md:p-6">
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<h1 className="text-foreground mb-3 text-xl">Scripts</h1>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { isUndefined } from 'lodash'
|
||||
import { ArrowUpRight, LogOut } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { ReactNode } from 'react'
|
||||
import { PropsWithChildren, ReactNode, useState } from 'react'
|
||||
|
||||
import { Badge, cn, Menu } from 'ui'
|
||||
import { Badge, cn, Menu, Sheet, SheetContent } from 'ui'
|
||||
import { LayoutHeader } from '../ProjectLayout/LayoutHeader'
|
||||
import type { SidebarLink, SidebarSection } from './AccountLayout.types'
|
||||
import MobileSheetNav from 'ui-patterns/MobileSheetNav/MobileSheetNav'
|
||||
|
||||
interface WithSidebarProps {
|
||||
title: string
|
||||
@@ -16,7 +17,6 @@ interface WithSidebarProps {
|
||||
subitemsParentKey?: number
|
||||
hideSidebar?: boolean
|
||||
customSidebarContent?: ReactNode
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const WithSidebar = ({
|
||||
@@ -29,63 +29,107 @@ const WithSidebar = ({
|
||||
subitemsParentKey,
|
||||
hideSidebar = false,
|
||||
customSidebarContent,
|
||||
}: WithSidebarProps) => {
|
||||
}: PropsWithChildren<WithSidebarProps>) => {
|
||||
const noContent = !sections && !customSidebarContent
|
||||
const [isSheetOpen, setIsSheetOpen] = useState(false)
|
||||
|
||||
const handleMobileMenu = () => {
|
||||
setIsSheetOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
<div className="flex flex-col md:flex-row h-full">
|
||||
{!hideSidebar && !noContent && (
|
||||
<div
|
||||
id="with-sidebar"
|
||||
className={[
|
||||
'h-full bg-dash-sidebar',
|
||||
'hide-scrollbar w-64 overflow-auto border-r border-default',
|
||||
].join(' ')}
|
||||
>
|
||||
{title && (
|
||||
<div className="mb-2">
|
||||
<div className="flex h-12 max-h-12 items-center border-b px-6 border-default">
|
||||
<h4 className="mb-0 text-lg truncate" title={title}>
|
||||
{title}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{header && header}
|
||||
<div className="-mt-1">
|
||||
<Menu>
|
||||
{customSidebarContent}
|
||||
{sections.map((section) => {
|
||||
return Boolean(section.heading) ? (
|
||||
<SectionWithHeaders
|
||||
key={section.key}
|
||||
section={section}
|
||||
subitems={subitems}
|
||||
subitemsParentKey={subitemsParentKey}
|
||||
/>
|
||||
) : (
|
||||
<div className="border-b py-5 px-6 border-default" key={section.key}>
|
||||
<SidebarItem
|
||||
links={section.links}
|
||||
subitems={subitems}
|
||||
subitemsParentKey={subitemsParentKey}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
<SidebarContent
|
||||
title={title}
|
||||
header={header}
|
||||
sections={sections}
|
||||
subitems={subitems}
|
||||
subitemsParentKey={subitemsParentKey}
|
||||
customSidebarContent={customSidebarContent}
|
||||
className="hidden md:block"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-1 flex-col">
|
||||
<LayoutHeader breadcrumbs={breadcrumbs} />
|
||||
<LayoutHeader
|
||||
breadcrumbs={breadcrumbs}
|
||||
showProductMenu={!hideSidebar && !noContent}
|
||||
handleMobileMenu={handleMobileMenu}
|
||||
/>
|
||||
<div className="flex-1 flex-grow overflow-y-auto">{children}</div>
|
||||
</div>
|
||||
<MobileSheetNav open={isSheetOpen} onOpenChange={setIsSheetOpen}>
|
||||
<SidebarContent
|
||||
title={title}
|
||||
header={header}
|
||||
sections={sections}
|
||||
subitems={subitems}
|
||||
subitemsParentKey={subitemsParentKey}
|
||||
customSidebarContent={customSidebarContent}
|
||||
/>
|
||||
</MobileSheetNav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default WithSidebar
|
||||
|
||||
export const SidebarContent = ({
|
||||
title,
|
||||
header,
|
||||
sections,
|
||||
subitems,
|
||||
subitemsParentKey,
|
||||
customSidebarContent,
|
||||
className,
|
||||
}: PropsWithChildren<Omit<WithSidebarProps, 'breadcrumbs'>> & { className?: string }) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
id="with-sidebar"
|
||||
className={cn(
|
||||
'h-full bg-dash-sidebar',
|
||||
'hide-scrollbar w-full md:w-64 overflow-auto md:border-r border-default',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{title && (
|
||||
<div className="block mb-2">
|
||||
<div className="flex h-12 max-h-12 items-center border-b px-6 border-default">
|
||||
<h4 className="mb-0 text-lg truncate" title={title}>
|
||||
{title}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{header && header}
|
||||
<div className="-mt-1">
|
||||
<Menu>
|
||||
{customSidebarContent}
|
||||
{sections.map((section) => {
|
||||
return Boolean(section.heading) ? (
|
||||
<SectionWithHeaders
|
||||
key={section.key}
|
||||
section={section}
|
||||
subitems={subitems}
|
||||
subitemsParentKey={subitemsParentKey}
|
||||
/>
|
||||
) : (
|
||||
<div className="border-b py-5 px-6 border-default" key={section.key}>
|
||||
<SidebarItem
|
||||
links={section.links}
|
||||
subitems={subitems}
|
||||
subitemsParentKey={subitemsParentKey}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface SectionWithHeadersProps {
|
||||
section: SidebarSection
|
||||
subitems?: any[]
|
||||
|
||||
@@ -77,7 +77,10 @@ const OrganizationLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
<ScaffoldTitle className="pb-3">
|
||||
{selectedOrganization?.name ?? 'Organization'} settings
|
||||
</ScaffoldTitle>
|
||||
<NavMenu className="border-none" aria-label="Organization menu navigation">
|
||||
<NavMenu
|
||||
className="border-none max-w-full overflow-hidden overflow-x-scroll"
|
||||
aria-label="Organization menu navigation"
|
||||
>
|
||||
{filteredNavMenuItems.map((item) => (
|
||||
<NavMenuItem key={item.label} active={currentPath === item.href}>
|
||||
<Link href={item.href}>{item.label}</Link>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Link from 'next/link'
|
||||
import { useMemo } from 'react'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
|
||||
import Connect from 'components/interfaces/Connect/Connect'
|
||||
import { useParams } from 'common'
|
||||
@@ -39,7 +39,22 @@ const LayoutHeaderDivider = () => (
|
||||
</span>
|
||||
)
|
||||
|
||||
const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = true }: any) => {
|
||||
interface LayoutHeaderProps {
|
||||
customHeaderComponents?: ReactNode
|
||||
breadcrumbs?: any[]
|
||||
headerBorder?: boolean
|
||||
showProductMenu?: boolean
|
||||
customSidebarContent?: ReactNode
|
||||
handleMobileMenu: Function
|
||||
}
|
||||
|
||||
const LayoutHeader = ({
|
||||
customHeaderComponents,
|
||||
breadcrumbs = [],
|
||||
headerBorder = true,
|
||||
showProductMenu,
|
||||
handleMobileMenu,
|
||||
}: LayoutHeaderProps) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const selectedProject = useSelectedProject()
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
@@ -72,51 +87,69 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
|
||||
headerBorder ? 'border-b border-default' : ''
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between py-2 px-3 flex-1">
|
||||
<div className="flex items-center text-sm">
|
||||
{projectRef && (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<OrganizationDropdown />
|
||||
<LayoutHeaderDivider />
|
||||
<ProjectDropdown />
|
||||
|
||||
{exceedingLimits && (
|
||||
<div className="ml-2">
|
||||
<Link href={`/org/${selectedOrganization?.slug}/usage`}>
|
||||
<Badge variant="destructive">Exceeding usage limits</Badge>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProject && isBranchingEnabled && (
|
||||
<>
|
||||
<LayoutHeaderDivider />
|
||||
<BranchDropdown />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="ml-3 flex items-center gap-x-3">
|
||||
{connectDialogUpdate && <Connect />}
|
||||
{!isBranchingEnabled && <EnableBranchingButton />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Additional breadcrumbs are supplied */}
|
||||
<BreadcrumbsView defaultValue={breadcrumbs} />
|
||||
{showProductMenu && (
|
||||
<div className="flex items-center justify-center border-r flex-0 md:hidden h-full aspect-square">
|
||||
<button
|
||||
title="Menu dropdown button"
|
||||
className={cn(
|
||||
'group/view-toggle ml-4 flex justify-center flex-col border-none space-x-0 items-start gap-1 !bg-transparent rounded-md min-w-[30px] w-[30px] h-[30px]'
|
||||
)}
|
||||
onClick={() => handleMobileMenu()}
|
||||
>
|
||||
<div className="h-px inline-block left-0 w-4 transition-all ease-out bg-foreground-lighter group-hover/view-toggle:bg-foreground p-0 m-0" />
|
||||
<div className="h-px inline-block left-0 w-3 transition-all ease-out bg-foreground-lighter group-hover/view-toggle:bg-foreground p-0 m-0" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
{customHeaderComponents && customHeaderComponents}
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
<FeedbackDropdown />
|
||||
<NotificationsPopoverV2 />
|
||||
<HelpPopover />
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
<div className="relative flex flex-1 overflow-hidden">
|
||||
<div className="flex w-full items-center justify-between py-2 pl-1 pr-3 md:px-3 flex-nowrap overflow-x-scroll">
|
||||
<div className="flex items-center text-sm">
|
||||
{projectRef && (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<OrganizationDropdown />
|
||||
<LayoutHeaderDivider />
|
||||
<ProjectDropdown />
|
||||
|
||||
{exceedingLimits && (
|
||||
<div className="ml-2">
|
||||
<Link href={`/org/${selectedOrganization?.slug}/usage`}>
|
||||
<Badge variant="destructive">Exceeding usage limits</Badge>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProject && isBranchingEnabled && (
|
||||
<>
|
||||
<LayoutHeaderDivider />
|
||||
<BranchDropdown />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="ml-3 flex items-center gap-x-3">
|
||||
{connectDialogUpdate && <Connect />}
|
||||
{!isBranchingEnabled && <EnableBranchingButton />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Additional breadcrumbs are supplied */}
|
||||
<BreadcrumbsView defaultValue={breadcrumbs} />
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
{customHeaderComponents && customHeaderComponents}
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
<FeedbackDropdown />
|
||||
<NotificationsPopoverV2 />
|
||||
<HelpPopover />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute md:hidden left-0 h-full w-3 bg-gradient-to-r from-background-dash-sidebar to-transparent pointer-events-none" />
|
||||
<div className="absolute md:hidden right-0 h-full w-3 bg-gradient-to-l from-background-dash-sidebar to-transparent pointer-events-none" />
|
||||
</div>
|
||||
{!!projectRef && (
|
||||
<div className="border-l flex-0 h-full">
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Search, Menu } from 'lucide-react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
|
||||
import { buttonVariants, cn, Sheet, SheetContent } from 'ui'
|
||||
import { CommandMenuTrigger } from 'ui-patterns'
|
||||
import { NavContent } from './NavigationBar'
|
||||
import { useState } from 'react'
|
||||
import MobileSheetNav from 'ui-patterns/MobileSheetNav/MobileSheetNav'
|
||||
|
||||
export const ICON_SIZE = 20
|
||||
export const ICON_STROKE_WIDTH = 1.5
|
||||
|
||||
const MobileNavigationBar = () => {
|
||||
const router = useRouter()
|
||||
const [isSheetOpen, setIsSheetOpen] = useState(false)
|
||||
const { ref: projectRef } = useParams()
|
||||
const snap = useAppStateSnapshot()
|
||||
|
||||
const onCloseNavigationIconLink = (event: any) => {
|
||||
snap.setNavigationPanelOpen(
|
||||
false,
|
||||
event.target.id === 'icon-link' || ['svg', 'path'].includes(event.target.localName)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-14 w-full flex flex-row md:hidden">
|
||||
<nav
|
||||
className={cn(
|
||||
'group px-4 z-10 w-full h-14',
|
||||
'border-b bg-dash-sidebar border-default shadow-xl',
|
||||
'transition-width duration-200',
|
||||
'hide-scrollbar flex flex-row items-center justify-between overflow-x-auto'
|
||||
)}
|
||||
>
|
||||
<Link
|
||||
href={IS_PLATFORM ? '/projects' : `/project/${projectRef}`}
|
||||
className="flex items-center h-[26px] w-[26px] min-w-[26px]"
|
||||
onClick={onCloseNavigationIconLink}
|
||||
>
|
||||
<img
|
||||
alt="Supabase"
|
||||
src={`${router.basePath}/img/supabase-logo.svg`}
|
||||
className="absolute h-[26px] w-[26px] cursor-pointer rounded"
|
||||
/>
|
||||
</Link>
|
||||
<div className="flex gap-2">
|
||||
<CommandMenuTrigger>
|
||||
<button
|
||||
className={cn(
|
||||
'group',
|
||||
'flex-grow h-[30px] rounded-md',
|
||||
'p-2',
|
||||
'flex items-center justify-between',
|
||||
'bg-transparent border-none text-foreground-lighter',
|
||||
'hover:bg-opacity-100 hover:border-strong hover:text-foreground-light',
|
||||
'focus-visible:!outline-4 focus-visible:outline-offset-1 focus-visible:outline-brand-600',
|
||||
'transition'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Search size={18} strokeWidth={2} />
|
||||
</div>
|
||||
</button>
|
||||
</CommandMenuTrigger>
|
||||
<button
|
||||
title="Menu dropdown button"
|
||||
className={cn(
|
||||
buttonVariants({ type: 'default' }),
|
||||
'flex lg:hidden border-default bg-surface-100/75 text-foreground-light rounded-md min-w-[30px] w-[30px] h-[30px] data-[state=open]:bg-overlay-hover/30'
|
||||
)}
|
||||
onClick={() => setIsSheetOpen(true)}
|
||||
>
|
||||
<Menu size={18} strokeWidth={1} />
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<MobileSheetNav open={isSheetOpen} onOpenChange={setIsSheetOpen}>
|
||||
<NavContent />
|
||||
</MobileSheetNav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MobileNavigationBar
|
||||
@@ -0,0 +1,39 @@
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { cn } from 'ui'
|
||||
|
||||
export const ICON_SIZE = 20
|
||||
export const ICON_STROKE_WIDTH = 1.5
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
handleMobileMenu: Function
|
||||
}
|
||||
|
||||
const MobileViewNav = ({ title, handleMobileMenu }: PropsWithChildren<Props>) => {
|
||||
return (
|
||||
<nav
|
||||
className={cn(
|
||||
'group px-4 z-10 w-full h-10',
|
||||
'border-b border-default',
|
||||
'transition-width duration-200',
|
||||
'flex md:hidden flex-row items-center gap-1 overflow-x-auto'
|
||||
)}
|
||||
>
|
||||
<button
|
||||
title="Menu dropdown button"
|
||||
className={cn(
|
||||
'group/view-toggle flex justify-center flex-col border-none space-x-0 items-start gap-1 !bg-transparent rounded-md min-w-[30px] w-[30px] h-[30px]'
|
||||
)}
|
||||
onClick={() => handleMobileMenu()}
|
||||
>
|
||||
<div className="h-px inline-block left-0 w-4 transition-all ease-out bg-foreground-lighter group-hover/view-toggle:bg-foreground p-0 m-0" />
|
||||
<div className="h-px inline-block left-0 w-3 transition-all ease-out bg-foreground-lighter group-hover/view-toggle:bg-foreground p-0 m-0" />
|
||||
</button>
|
||||
<div className="flex items-center">
|
||||
<h4 className="text-base">{title}</h4>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default MobileViewNav
|
||||
@@ -15,7 +15,6 @@ import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
|
||||
import { useFlag } from 'hooks/ui/useFlag'
|
||||
import { useSignOut } from 'lib/auth'
|
||||
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
|
||||
import { detectOS } from 'lib/helpers'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import {
|
||||
@@ -57,7 +56,37 @@ export const ICON_SIZE = 20
|
||||
export const ICON_STROKE_WIDTH = 1.5
|
||||
|
||||
const NavigationBar = () => {
|
||||
const os = detectOS()
|
||||
const snap = useAppStateSnapshot()
|
||||
|
||||
const [userDropdownOpen, setUserDropdownOpenState] = useState(false)
|
||||
|
||||
const [allowNavPanelToExpand] = useLocalStorageQuery(
|
||||
LOCAL_STORAGE_KEYS.EXPAND_NAVIGATION_PANEL,
|
||||
true
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="w-14 h-full hidden md:flex flex-col">
|
||||
<nav
|
||||
data-state={snap.navigationPanelOpen ? 'expanded' : 'collapsed'}
|
||||
className={cn(
|
||||
'group py-2 z-10 h-full w-[13rem] md:w-14 md:data-[state=expanded]:w-[13rem]',
|
||||
'border-r bg-dash-sidebar border-default data-[state=expanded]:shadow-xl',
|
||||
'transition-width duration-200',
|
||||
'hide-scrollbar flex flex-col justify-between overflow-y-auto'
|
||||
)}
|
||||
onMouseEnter={() => allowNavPanelToExpand && snap.setNavigationPanelOpen(true)}
|
||||
onMouseLeave={() => {
|
||||
if (!userDropdownOpen && allowNavPanelToExpand) snap.setNavigationPanelOpen(false)
|
||||
}}
|
||||
>
|
||||
<NavContent />
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const NavContent = () => {
|
||||
const router = useRouter()
|
||||
const { profile } = useProfile()
|
||||
const { project } = useProjectContext()
|
||||
@@ -169,9 +198,9 @@ const NavigationBar = () => {
|
||||
</figure>
|
||||
<span
|
||||
className={cn(
|
||||
'w-[8rem] flex flex-col items-start text-sm truncate',
|
||||
'absolute left-7 group-data-[state=expanded]:left-10',
|
||||
'group-data-[state=collapsed]:opacity-0 group-data-[state=expanded]:opacity-100',
|
||||
'w-full md:w-[8rem] flex flex-col items-start text-sm truncate',
|
||||
'absolute left-10 md:left-7 group-data-[state=expanded]:left-10',
|
||||
'opacity-100 md:group-data-[state=collapsed]:opacity-0 md:group-data-[state=expanded]:opacity-100',
|
||||
'transition-all'
|
||||
)}
|
||||
>
|
||||
@@ -196,247 +225,229 @@ const NavigationBar = () => {
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="w-14 h-full flex flex-col">
|
||||
<nav
|
||||
data-state={snap.navigationPanelOpen ? 'expanded' : 'collapsed'}
|
||||
className={cn(
|
||||
'group py-2 z-10 h-full w-14 data-[state=expanded]:w-[13rem]',
|
||||
'border-r bg-dash-sidebar border-default data-[state=expanded]:shadow-xl',
|
||||
'transition-width duration-200',
|
||||
'hide-scrollbar flex flex-col justify-between overflow-y-auto'
|
||||
)}
|
||||
onMouseEnter={() => allowNavPanelToExpand && snap.setNavigationPanelOpen(true)}
|
||||
onMouseLeave={() => {
|
||||
if (!userDropdownOpen && allowNavPanelToExpand) snap.setNavigationPanelOpen(false)
|
||||
}}
|
||||
>
|
||||
<ul className="flex flex-col gap-y-1 justify-start px-2 relative">
|
||||
<Link
|
||||
href={IS_PLATFORM ? '/projects' : `/project/${projectRef}`}
|
||||
className="mx-2 flex items-center h-[40px]"
|
||||
onClick={onCloseNavigationIconLink}
|
||||
>
|
||||
<img
|
||||
alt="Supabase"
|
||||
src={`${router.basePath}/img/supabase-logo.svg`}
|
||||
className="absolute h-[40px] w-6 cursor-pointer rounded"
|
||||
/>
|
||||
</Link>
|
||||
<>
|
||||
<ul className="flex flex-col gap-y-1 justify-start px-2 relative">
|
||||
<Link
|
||||
href={IS_PLATFORM ? '/projects' : `/project/${projectRef}`}
|
||||
className="mx-2 hidden md:flex items-center w-[40px] h-[40px]"
|
||||
onClick={onCloseNavigationIconLink}
|
||||
>
|
||||
<img
|
||||
alt="Supabase"
|
||||
src={`${router.basePath}/img/supabase-logo.svg`}
|
||||
className="absolute h-[40px] w-6 cursor-pointer rounded"
|
||||
/>
|
||||
</Link>
|
||||
<NavigationIconLink
|
||||
isActive={isUndefined(activeRoute) && !isUndefined(router.query.ref)}
|
||||
route={{
|
||||
key: 'HOME',
|
||||
label: 'Home',
|
||||
icon: <Home size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
|
||||
link: `/project/${projectRef}`,
|
||||
linkElement: <ProjectIndexPageLink projectRef={projectRef} />,
|
||||
}}
|
||||
onClick={onCloseNavigationIconLink}
|
||||
/>
|
||||
<Separator className="my-1 bg-border-muted" />
|
||||
{toolRoutes.map((route) => (
|
||||
<NavigationIconLink
|
||||
isActive={isUndefined(activeRoute) && !isUndefined(router.query.ref)}
|
||||
route={{
|
||||
key: 'HOME',
|
||||
label: 'Home',
|
||||
icon: <Home size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
|
||||
link: `/project/${projectRef}`,
|
||||
linkElement: <ProjectIndexPageLink projectRef={projectRef} />,
|
||||
}}
|
||||
key={route.key}
|
||||
route={route}
|
||||
isActive={activeRoute === route.key}
|
||||
onClick={onCloseNavigationIconLink}
|
||||
/>
|
||||
<Separator className="my-1 bg-border-muted" />
|
||||
{toolRoutes.map((route) => (
|
||||
<NavigationIconLink
|
||||
key={route.key}
|
||||
route={route}
|
||||
isActive={activeRoute === route.key}
|
||||
onClick={onCloseNavigationIconLink}
|
||||
/>
|
||||
))}
|
||||
<Separator className="my-1 bg-border-muted" />
|
||||
{productRoutes.map((route) => (
|
||||
<NavigationIconLink
|
||||
key={route.key}
|
||||
route={route}
|
||||
isActive={activeRoute === route.key}
|
||||
onClick={onCloseNavigationIconLink}
|
||||
/>
|
||||
))}
|
||||
))}
|
||||
<Separator className="my-1 bg-border-muted" />
|
||||
{productRoutes.map((route) => (
|
||||
<NavigationIconLink
|
||||
key={route.key}
|
||||
route={route}
|
||||
isActive={activeRoute === route.key}
|
||||
onClick={onCloseNavigationIconLink}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Separator className="my-1 bg-border-muted" />
|
||||
{otherRoutes.map((route) => {
|
||||
if (route.key === 'api' && isNewAPIDocsEnabled) {
|
||||
return (
|
||||
<NavigationIconButton
|
||||
key={route.key}
|
||||
onClick={() => {
|
||||
snap.setShowProjectApiDocs(true)
|
||||
snap.setNavigationPanelOpen(false)
|
||||
}}
|
||||
icon={<FileText size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />}
|
||||
>
|
||||
Project API
|
||||
</NavigationIconButton>
|
||||
)
|
||||
} else if (route.key === 'advisors') {
|
||||
return (
|
||||
<div className="relative" key={route.key}>
|
||||
{securityLints.length > 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute flex h-2 w-2 left-6 top-2 z-10 rounded-full',
|
||||
errorLints.length > 0 ? 'bg-destructive-600' : 'bg-warning-600'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<NavigationIconLink
|
||||
route={route}
|
||||
isActive={activeRoute === route.key}
|
||||
onClick={onCloseNavigationIconLink}
|
||||
<Separator className="my-1 bg-border-muted" />
|
||||
{otherRoutes.map((route) => {
|
||||
if (route.key === 'api' && isNewAPIDocsEnabled) {
|
||||
return (
|
||||
<NavigationIconButton
|
||||
key={route.key}
|
||||
onClick={() => {
|
||||
snap.setShowProjectApiDocs(true)
|
||||
snap.setNavigationPanelOpen(false)
|
||||
}}
|
||||
icon={<FileText size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />}
|
||||
>
|
||||
Project API
|
||||
</NavigationIconButton>
|
||||
)
|
||||
} else if (route.key === 'advisors') {
|
||||
return (
|
||||
<div className="relative" key={route.key}>
|
||||
{securityLints.length > 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute flex h-2 w-2 left-6 top-2 z-10 rounded-full',
|
||||
errorLints.length > 0 ? 'bg-destructive-600' : 'bg-warning-600'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
} else if (route.key === 'logs') {
|
||||
// TODO: Undo this when warehouse flag is removed
|
||||
const label = showWarehouse ? 'Logs & Analytics' : route.label
|
||||
const newRoute = { ...route, label }
|
||||
return (
|
||||
)}
|
||||
|
||||
<NavigationIconLink
|
||||
key={newRoute.key}
|
||||
route={newRoute}
|
||||
isActive={activeRoute === newRoute.key}
|
||||
onClick={onCloseNavigationIconLink}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<NavigationIconLink
|
||||
key={route.key}
|
||||
route={route}
|
||||
isActive={activeRoute === route.key}
|
||||
onClick={onCloseNavigationIconLink}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
} else if (route.key === 'logs') {
|
||||
// TODO: Undo this when warehouse flag is removed
|
||||
const label = showWarehouse ? 'Logs & Analytics' : route.label
|
||||
const newRoute = { ...route, label }
|
||||
return (
|
||||
<NavigationIconLink
|
||||
key={newRoute.key}
|
||||
route={newRoute}
|
||||
isActive={activeRoute === newRoute.key}
|
||||
onClick={onCloseNavigationIconLink}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<NavigationIconLink
|
||||
key={route.key}
|
||||
route={route}
|
||||
isActive={activeRoute === route.key}
|
||||
onClick={onCloseNavigationIconLink}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
|
||||
<ul className="flex flex-col px-2 gap-y-1">
|
||||
{settingsRoutes.map((route) => (
|
||||
<NavigationIconLink
|
||||
key={route.key}
|
||||
route={route}
|
||||
isActive={activeRoute === route.key}
|
||||
onClick={onCloseNavigationIconLink}
|
||||
/>
|
||||
))}
|
||||
<ul className="flex flex-col px-2 pb-4 md:pb-0 gap-y-1">
|
||||
{settingsRoutes.map((route) => (
|
||||
<NavigationIconLink
|
||||
key={route.key}
|
||||
route={route}
|
||||
isActive={activeRoute === route.key}
|
||||
onClick={onCloseNavigationIconLink}
|
||||
/>
|
||||
))}
|
||||
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
{!allowNavPanelToExpand && (
|
||||
<Tooltip_Shadcn_>
|
||||
<TooltipTrigger_Shadcn_ asChild>{CommandButton}</TooltipTrigger_Shadcn_>
|
||||
<TooltipContent_Shadcn_ side="right">
|
||||
<span>Commands</span>
|
||||
</TooltipContent_Shadcn_>
|
||||
</Tooltip_Shadcn_>
|
||||
)}
|
||||
{allowNavPanelToExpand && CommandButton}
|
||||
</>
|
||||
)}
|
||||
|
||||
<DropdownMenu
|
||||
open={userDropdownOpen}
|
||||
onOpenChange={(open: boolean) => {
|
||||
setUserDropdownOpenState(open)
|
||||
if (open === false) snap.setNavigationPanelOpen(false)
|
||||
}}
|
||||
>
|
||||
{allowNavPanelToExpand ? (
|
||||
<DropdownMenuTrigger asChild>{UserAccountButton}</DropdownMenuTrigger>
|
||||
) : (
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
{!allowNavPanelToExpand && (
|
||||
<Tooltip_Shadcn_>
|
||||
<TooltipTrigger_Shadcn_ asChild>
|
||||
<DropdownMenuTrigger asChild>{UserAccountButton}</DropdownMenuTrigger>
|
||||
</TooltipTrigger_Shadcn_>
|
||||
<TooltipTrigger_Shadcn_ asChild>{CommandButton}</TooltipTrigger_Shadcn_>
|
||||
<TooltipContent_Shadcn_ side="right">
|
||||
<span>Account settings</span>
|
||||
<span>Commands</span>
|
||||
</TooltipContent_Shadcn_>
|
||||
</Tooltip_Shadcn_>
|
||||
)}
|
||||
{allowNavPanelToExpand && CommandButton}
|
||||
</>
|
||||
)}
|
||||
|
||||
<DropdownMenuContent side="top" align="start">
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
<div className="px-2 py-1 flex flex-col gap-0 text-sm">
|
||||
{profile && (
|
||||
<>
|
||||
<DropdownMenu
|
||||
open={userDropdownOpen}
|
||||
onOpenChange={(open: boolean) => {
|
||||
setUserDropdownOpenState(open)
|
||||
if (open === false) snap.setNavigationPanelOpen(false)
|
||||
}}
|
||||
>
|
||||
{allowNavPanelToExpand ? (
|
||||
<DropdownMenuTrigger asChild>{UserAccountButton}</DropdownMenuTrigger>
|
||||
) : (
|
||||
<Tooltip_Shadcn_>
|
||||
<TooltipTrigger_Shadcn_ asChild>
|
||||
<DropdownMenuTrigger asChild>{UserAccountButton}</DropdownMenuTrigger>
|
||||
</TooltipTrigger_Shadcn_>
|
||||
<TooltipContent_Shadcn_ side="right">
|
||||
<span>Account settings</span>
|
||||
</TooltipContent_Shadcn_>
|
||||
</Tooltip_Shadcn_>
|
||||
)}
|
||||
|
||||
<DropdownMenuContent side="top" align="start">
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
<div className="px-2 py-1 flex flex-col gap-0 text-sm">
|
||||
{profile && (
|
||||
<>
|
||||
<span
|
||||
title={profile.username}
|
||||
className="w-full text-left text-foreground truncate"
|
||||
>
|
||||
{profile.username}
|
||||
</span>
|
||||
{profile.primary_email !== profile.username && (
|
||||
<span
|
||||
title={profile.username}
|
||||
className="w-full text-left text-foreground truncate"
|
||||
title={profile.primary_email}
|
||||
className="w-full text-left text-foreground-light text-xs truncate"
|
||||
>
|
||||
{profile.username}
|
||||
{profile.primary_email}
|
||||
</span>
|
||||
{profile.primary_email !== profile.username && (
|
||||
<span
|
||||
title={profile.primary_email}
|
||||
className="w-full text-left text-foreground-light text-xs truncate"
|
||||
>
|
||||
{profile.primary_email}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem className="flex gap-2" asChild>
|
||||
<Link href="/account/me">
|
||||
<Settings size={14} strokeWidth={1.5} className="text-foreground-lighter" />
|
||||
Account preferences
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="flex gap-2"
|
||||
onClick={() => snap.setShowFeaturePreviewModal(true)}
|
||||
onSelect={() => snap.setShowFeaturePreviewModal(true)}
|
||||
>
|
||||
<FlaskConical size={14} strokeWidth={1.5} className="text-foreground-lighter" />
|
||||
Feature previews
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem className="flex gap-2" asChild>
|
||||
<Link href="/account/me">
|
||||
<Settings size={14} strokeWidth={1.5} className="text-foreground-lighter" />
|
||||
Account preferences
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="flex gap-2"
|
||||
onClick={() => snap.setShowFeaturePreviewModal(true)}
|
||||
onSelect={() => snap.setShowFeaturePreviewModal(true)}
|
||||
>
|
||||
<FlaskConical
|
||||
size={14}
|
||||
strokeWidth={1.5}
|
||||
className="text-foreground-lighter"
|
||||
/>
|
||||
Feature previews
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</DropdownMenuGroup>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Theme</DropdownMenuLabel>
|
||||
<DropdownMenuRadioGroup
|
||||
value={theme}
|
||||
onValueChange={(value) => {
|
||||
setTheme(value)
|
||||
}}
|
||||
>
|
||||
{singleThemes.map((theme: Theme) => (
|
||||
<DropdownMenuRadioItem key={theme.value} value={theme.value}>
|
||||
{theme.name}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuGroup>
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
onSelect={async () => {
|
||||
await signOut()
|
||||
await router.push('/sign-in')
|
||||
}}
|
||||
>
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</DropdownMenuGroup>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Theme</DropdownMenuLabel>
|
||||
<DropdownMenuRadioGroup
|
||||
value={theme}
|
||||
onValueChange={(value) => {
|
||||
setTheme(value)
|
||||
}}
|
||||
>
|
||||
{singleThemes.map((theme: Theme) => (
|
||||
<DropdownMenuRadioItem key={theme.value} value={theme.value}>
|
||||
{theme.name}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuGroup>
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
onSelect={async () => {
|
||||
await signOut()
|
||||
await router.push('/sign-in')
|
||||
}}
|
||||
>
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ export const NavigationIconButton = forwardRef<
|
||||
<div className="absolute left-2 text-foreground-lighter">{icon}</div>
|
||||
<span
|
||||
className={cn(
|
||||
'absolute left-7 group-data-[state=expanded]:left-10',
|
||||
'opacity-0 group-data-[state=expanded]:opacity-100',
|
||||
'absolute left-10 md:left-7 md:group-data-[state=expanded]:left-10',
|
||||
'opacity-100 md:opacity-0 md:group-data-[state=expanded]:opacity-100',
|
||||
'w-[10rem] text-sm flex flex-col items-center',
|
||||
'transition-all'
|
||||
)}
|
||||
@@ -38,8 +38,8 @@ export const NavigationIconButton = forwardRef<
|
||||
<div
|
||||
className={cn(
|
||||
'absolute right-2 flex items-center',
|
||||
'opacity-0 transition-all',
|
||||
'group-data-[state=expanded]:opacity-100 '
|
||||
'opacity-100 md:opacity-0 transition-all',
|
||||
'md:group-data-[state=expanded]:opacity-100 '
|
||||
)}
|
||||
>
|
||||
{rightText}
|
||||
|
||||
@@ -36,7 +36,7 @@ const NavigationIconLink = forwardRef<HTMLAnchorElement, NavigationIconButtonPro
|
||||
|
||||
const classes = [
|
||||
'relative',
|
||||
'h-10 w-10 group-data-[state=expanded]:w-full',
|
||||
'h-10 w-full md:w-10 md:group-data-[state=expanded]:w-full',
|
||||
'transition-all duration-200',
|
||||
'flex items-center rounded',
|
||||
'group-data-[state=collapsed]:justify-center',
|
||||
@@ -80,8 +80,8 @@ const NavigationIconLink = forwardRef<HTMLAnchorElement, NavigationIconButtonPro
|
||||
'min-w-[128px] text-sm text-foreground-light',
|
||||
'group-hover/item:text-foreground',
|
||||
'group-aria-current/item:text-foreground',
|
||||
'absolute left-7 group-data-[state=expanded]:left-12',
|
||||
'opacity-0 group-data-[state=expanded]:opacity-100',
|
||||
'absolute left-10 md:left-7 md:group-data-[state=expanded]:left-12',
|
||||
'opacity-100 md:opacity-0 md:group-data-[state=expanded]:opacity-100',
|
||||
`${isActive && 'text-foreground hover:text-foreground'}`,
|
||||
'transition-all'
|
||||
)}
|
||||
|
||||
@@ -7,6 +7,10 @@ interface ProductMenuBarProps {
|
||||
const ProductMenuBar = ({ title, children }: PropsWithChildren<ProductMenuBarProps>) => {
|
||||
return (
|
||||
<div
|
||||
/**
|
||||
* id used in playwright-tests/tests/snapshot/spec/table-editor.spec.ts
|
||||
* */
|
||||
id="spec-click-target"
|
||||
className={[
|
||||
'hide-scrollbar flex flex-col w-full h-full', // Layout
|
||||
'bg-dash-sidebar',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
import { forwardRef, Fragment, PropsWithChildren, ReactNode, useEffect, useState } from 'react'
|
||||
import {
|
||||
forwardRef,
|
||||
Fragment,
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import ProjectAPIDocs from 'components/interfaces/ProjectAPIDocs/ProjectAPIDocs'
|
||||
@@ -14,7 +22,7 @@ import { withAuth } from 'hooks/misc/withAuth'
|
||||
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
import { cn, ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'ui'
|
||||
import { cn, ResizableHandle, ResizablePanel, ResizablePanelGroup, Sheet, SheetContent } from 'ui'
|
||||
import AppLayout from '../AppLayout/AppLayout'
|
||||
import EnableBranchingModal from '../AppLayout/EnableBranchingButton/EnableBranchingModal'
|
||||
import BuildingState from './BuildingState'
|
||||
@@ -22,6 +30,8 @@ import ConnectingState from './ConnectingState'
|
||||
import { LayoutHeader } from './LayoutHeader'
|
||||
import LoadingState from './LoadingState'
|
||||
import NavigationBar from './NavigationBar/NavigationBar'
|
||||
import MobileNavigationBar from './NavigationBar/MobileNavigationBar'
|
||||
import MobileViewNav from './NavigationBar/MobileViewNav'
|
||||
import { ProjectPausedState } from './PausedState/ProjectPausedState'
|
||||
import PauseFailedState from './PauseFailedState'
|
||||
import PausingState from './PausingState'
|
||||
@@ -32,6 +42,7 @@ import RestartingState from './RestartingState'
|
||||
import RestoreFailedState from './RestoreFailedState'
|
||||
import RestoringState from './RestoringState'
|
||||
import { UpgradingState } from './UpgradingState'
|
||||
import MobileSheetNav from 'ui-patterns/MobileSheetNav/MobileSheetNav'
|
||||
|
||||
// [Joshen] This is temporary while we unblock users from managing their project
|
||||
// if their project is not responding well for any reason. Eventually needs a bit of an overhaul
|
||||
@@ -87,6 +98,7 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
|
||||
ref
|
||||
) => {
|
||||
const router = useRouter()
|
||||
const [isSheetOpen, setIsSheetOpen] = useState(false)
|
||||
const { ref: projectRef } = useParams()
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const selectedProject = useSelectedProject()
|
||||
@@ -126,6 +138,10 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open])
|
||||
|
||||
const handleMobileMenu = () => {
|
||||
setIsSheetOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<ProjectContextProvider projectRef={projectRef}>
|
||||
@@ -143,21 +159,32 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
|
||||
</title>
|
||||
<meta name="description" content="Supabase Studio" />
|
||||
</Head>
|
||||
<div className="flex h-full">
|
||||
<div className="flex flex-col md:flex-row h-full">
|
||||
{/* Left-most navigation side bar to access products */}
|
||||
{!hideIconBar && <NavigationBar />}
|
||||
{/* Top Nav to access products from mobile */}
|
||||
{!hideIconBar && <MobileNavigationBar />}
|
||||
{showProductMenu && productMenu && !(!hideHeader && IS_PLATFORM) && (
|
||||
<MobileViewNav title={product} handleMobileMenu={handleMobileMenu} />
|
||||
)}
|
||||
|
||||
{/* Product menu bar */}
|
||||
<ResizablePanelGroup
|
||||
className="flex h-full"
|
||||
direction="horizontal"
|
||||
autoSaveId="project-layout"
|
||||
>
|
||||
{/* Existing desktop menu */}
|
||||
<ResizablePanel
|
||||
id="panel-left"
|
||||
className={cn(resizableSidebar ? 'min-w-64 max-w-[32rem]' : 'min-w-64 max-w-64', {
|
||||
hidden: !showProductMenu || !productMenu,
|
||||
})}
|
||||
defaultSize={0} // forces panel to smallest width possible, at w-64
|
||||
className={cn(
|
||||
'hidden md:flex',
|
||||
resizableSidebar ? 'min-w-64 max-w-[32rem]' : 'min-w-64 max-w-64',
|
||||
{
|
||||
'!hidden': !showProductMenu || !productMenu,
|
||||
}
|
||||
)}
|
||||
defaultSize={0}
|
||||
>
|
||||
<MenuBarWrapper
|
||||
isLoading={isLoading}
|
||||
@@ -168,18 +195,23 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
|
||||
</MenuBarWrapper>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle
|
||||
className={cn({ hidden: !showProductMenu || !productMenu })}
|
||||
className={cn('hidden md:flex', { '!hidden': !showProductMenu || !productMenu })}
|
||||
withHandle
|
||||
disabled={resizableSidebar ? false : true}
|
||||
/>
|
||||
<ResizablePanel id="panel-right" className="h-full flex flex-col">
|
||||
{!hideHeader && IS_PLATFORM && <LayoutHeader />}
|
||||
{!hideHeader && IS_PLATFORM && (
|
||||
<LayoutHeader
|
||||
showProductMenu={!!(showProductMenu && productMenu)}
|
||||
handleMobileMenu={handleMobileMenu}
|
||||
/>
|
||||
)}
|
||||
<ResizablePanelGroup
|
||||
className="h-full w-full overflow-x-hidden flex-1"
|
||||
direction="horizontal"
|
||||
autoSaveId="project-layout-content"
|
||||
>
|
||||
<ResizablePanel id="panel-content" className=" w-full min-w-[600px]">
|
||||
<ResizablePanel id="panel-content" className="w-full md:min-w-[600px]">
|
||||
<main
|
||||
className="h-full flex flex-col flex-1 w-full overflow-y-auto overflow-x-hidden"
|
||||
ref={ref}
|
||||
@@ -222,6 +254,9 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
|
||||
<AISettingsModal />
|
||||
<ProjectAPIDocs />
|
||||
</ProjectContextProvider>
|
||||
<MobileSheetNav open={isSheetOpen} onOpenChange={setIsSheetOpen}>
|
||||
{productMenu}
|
||||
</MobileSheetNav>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useAppStateSnapshot } from 'state/app-state'
|
||||
import { cn } from 'ui'
|
||||
|
||||
export const MAX_WIDTH_CLASSES = 'mx-auto w-full max-w-[1200px]'
|
||||
export const PADDING_CLASSES = 'px-6 lg:px-14 xl:px-24 2xl:px-32'
|
||||
export const PADDING_CLASSES = 'px-4 md:px-6 lg:px-14 xl:px-24 2xl:px-32'
|
||||
export const MAX_WIDTH_CLASSES_COLUMN = 'min-w-[420px]'
|
||||
|
||||
const ScaffoldHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
|
||||
@@ -39,12 +39,14 @@ import {
|
||||
} from 'ui-patterns/InnerSideMenu'
|
||||
import { useProjectContext } from '../ProjectLayout/ProjectContext'
|
||||
import EntityListItem from './EntityListItem'
|
||||
import { useBreakpoint } from 'common/hooks/useBreakpoint'
|
||||
|
||||
const TableEditorMenu = () => {
|
||||
const { id: _id } = useParams()
|
||||
const id = _id ? Number(_id) : undefined
|
||||
const snap = useTableEditorStateSnapshot()
|
||||
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
|
||||
const isMobile = useBreakpoint()
|
||||
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [searchText, setSearchText] = useState<string>('')
|
||||
@@ -169,7 +171,7 @@ const TableEditorMenu = () => {
|
||||
<div className="flex flex-auto flex-col gap-2 pb-4 px-2">
|
||||
<InnerSideBarFilters>
|
||||
<InnerSideBarFilterSearchInput
|
||||
autoFocus
|
||||
autoFocus={!isMobile}
|
||||
name="search-tables"
|
||||
aria-labelledby="Search tables"
|
||||
onChange={(e) => {
|
||||
@@ -201,7 +203,7 @@ const TableEditorMenu = () => {
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
type={visibleTypes.length !== 5 ? 'default' : 'dashed'}
|
||||
className="h-[28px] px-1.5"
|
||||
className="h-[32px] md:h-[28px] px-1.5"
|
||||
icon={<Filter />}
|
||||
/>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
|
||||
@@ -24,7 +24,7 @@ const MigrationsPage: NextPageWithLayout = () => {
|
||||
description="History of migrations that have been run on your database"
|
||||
/>
|
||||
</ScaffoldSectionContent>
|
||||
<ScaffoldSectionDetail className="flex items-center justify-end gap-x-2">
|
||||
<ScaffoldSectionDetail className="flex items-center md:justify-end gap-x-2">
|
||||
<DocsButton
|
||||
className="no-underline"
|
||||
href="https://supabase.com/docs/guides/deployment/database-migrations"
|
||||
|
||||
@@ -145,8 +145,8 @@ const InnerSideBarFilterSearchInput = forwardRef<
|
||||
ref={ref}
|
||||
type="text"
|
||||
className={cn(
|
||||
'h-[28px] w-full',
|
||||
'text-xs',
|
||||
'h-[32px] md:h-[28px] w-full',
|
||||
'text-base md:text-xs',
|
||||
'pl-7',
|
||||
'pr-7',
|
||||
'w-full',
|
||||
@@ -179,7 +179,7 @@ const InnerSideBarFilterSortDropdown = forwardRef<
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
className={cn(
|
||||
'absolute right-1 top-[.3rem]',
|
||||
'absolute right-1 top-[.4rem] md:top-[.3rem]',
|
||||
'text-foreground-muted transition-colors hover:text-foreground data-[state=open]:text-foreground',
|
||||
triggerClassName
|
||||
)}
|
||||
|
||||
44
packages/ui-patterns/MobileSheetNav/MobileSheetNav.tsx
Normal file
44
packages/ui-patterns/MobileSheetNav/MobileSheetNav.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
'use client'
|
||||
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
import { ErrorBoundary } from 'react-error-boundary'
|
||||
import { useWindowSize } from 'react-use'
|
||||
import { CommandEmpty_Shadcn_, Sheet, SheetContent } from 'ui'
|
||||
import { cn } from 'ui/src/lib/utils'
|
||||
|
||||
const MobileSheetNav: React.FC<{
|
||||
children: React.ReactNode
|
||||
open?: boolean
|
||||
onOpenChange(open: boolean): void
|
||||
}> = ({ children, open = false, onOpenChange }) => {
|
||||
const router = useRouter()
|
||||
const { width } = useWindowSize()
|
||||
|
||||
useEffect(() => {
|
||||
onOpenChange(false)
|
||||
}, [router?.asPath])
|
||||
|
||||
useEffect(() => {
|
||||
onOpenChange(false)
|
||||
}, [width])
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent
|
||||
id="mobile-sheet-content"
|
||||
showClose={false}
|
||||
size="full"
|
||||
side="bottom"
|
||||
className={cn(
|
||||
'rounded-t-lg overflow-hidden overflow-y-scroll',
|
||||
'h-[85dvh] md:max-h-[500px] py-2'
|
||||
)}
|
||||
>
|
||||
<ErrorBoundary FallbackComponent={() => <CommandEmpty_Shadcn_ />}>{children}</ErrorBoundary>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
export default MobileSheetNav
|
||||
1
packages/ui-patterns/MobileSheetNav/index.ts
Normal file
1
packages/ui-patterns/MobileSheetNav/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './MobileSheetNav'
|
||||
@@ -8,6 +8,7 @@ export * from './CountdownWidget'
|
||||
export * from './ExpandableVideo'
|
||||
export * from './GlassPanel'
|
||||
export * from './IconPanel'
|
||||
export * from './MobileSheetNav'
|
||||
export * from './PrivacySettings'
|
||||
export * from './ThemeToggle'
|
||||
export * from './TweetCard'
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface InputProps
|
||||
VariantProps<typeof InputVariants> {}
|
||||
|
||||
export const InputVariants = cva(
|
||||
'aria-[] flex h-10 w-full rounded-md border border-control bg-foreground/[.026] px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-foreground-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-background-control focus-visible:ring-offset-2 focus-visible:ring-offset-foreground-muted disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid=true]:bg-destructive-200 aria-[invalid=true]:border-destructive-400 aria-[invalid=true]:focus:border-destructive aria-[invalid=true]:focus-visible:border-destructive',
|
||||
'aria-[] flex h-10 w-full rounded-md border border-control bg-foreground/[.026] px-3 py-2 text-base md:text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-foreground-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-background-control focus-visible:ring-offset-2 focus-visible:ring-offset-foreground-muted disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid=true]:bg-destructive-200 aria-[invalid=true]:border-destructive-400 aria-[invalid=true]:focus:border-destructive aria-[invalid=true]:focus-visible:border-destructive',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
|
||||
@@ -77,11 +77,10 @@ test.describe('Table Editor page', () => {
|
||||
await page.getByTestId('table-editor-pick-column-to-sort-button').click()
|
||||
await page.getByLabel('Pick a column to sort by').getByText('defaultValueColumn').click()
|
||||
await page.getByRole('button', { name: 'Apply sorting' }).click()
|
||||
|
||||
// click away to close the sorting dialog
|
||||
await page
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Table Editor$/ })
|
||||
.click()
|
||||
await page.locator('#spec-click-target').click()
|
||||
|
||||
// expect the row to be sorted by defaultValueColumn. They're inserted in the order 100, 2
|
||||
await expect(page.locator('div.rdg-row:nth-child(2)')).toContainText('2')
|
||||
await expect(page.locator('div.rdg-row:nth-child(3)')).toContainText('100')
|
||||
@@ -98,10 +97,7 @@ test.describe('Table Editor page', () => {
|
||||
await page.getByPlaceholder('Enter a value').fill('2')
|
||||
await page.getByRole('button', { name: 'Apply filter' }).click()
|
||||
// click away to close the filter dialog
|
||||
await page
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Table Editor$/ })
|
||||
.click()
|
||||
await page.locator('#spec-click-target').click()
|
||||
await expect(page.getByRole('grid')).toContainText('2')
|
||||
await expect(page.getByRole('grid')).not.toContainText('100')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user