mirror of
https://github.com/supabase/supabase.git
synced 2026-07-25 00:42:58 -04:00
4b7cb27ba9
## Problem Now that `docs` is the only place where we use the deprecated `ui/Tabs`, we can move this component and the related HOC from `ui-patterns` in `docs` ## Solution - Move `ui/Tabs`, `ui-patterns/ComplexTabs/withQueryParams` and `ui-patterns/ComplexTabs/withSticky` to `docs` - Refactor `ui-patterns/ComplexTabs/withQueryParams` and `ui-patterns/ComplexTabs/withSticky` HOCs as hooks to make them easier to understand - Refactor `Tabs` accordingly No visual nor functional changes. ## How to test On https://docs-git-chore-refactor-docs-tabs-supabase.vercel.app/docs/guides/auth/passwords (Tabs are driven by URL and the flow tabs should have sticky headers even though there's a CSS bug already reported) - check that by default, the first tab in each tabs is active - change the tabs in different groups and validate it works - refresh the page and check that previously selected tabs are active (URL based selection) - In a new tab, visit https://docs-git-chore-refactor-docs-tabs-supabase.vercel.app/docs/guides/auth/passwords again and check that previously selected tabs are active (LocalStorage based selection) Do the same on https://docs-git-chore-refactor-docs-tabs-supabase.vercel.app/docs/guides/database/database-advisors (This one is driven by URL but does not have sticky tab headers) Do the same on https://docs-git-chore-refactor-docs-tabs-supabase.vercel.app/docs/guides/deployment/terraform/reference (this one is not driven by URL nor has sticky tab headers) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Docs tabs now persist and restore the active tab via URL query parameters. * Added optional “sticky” tab behavior that keeps the active panel in view. * Enhanced keyboard interaction for selecting tabs. * **Bug Fixes** * Improved active-tab initialization and synchronization when the URL query changes. * **Chores** * Refreshed the tabs UI implementation and styling to improve consistency and remove deprecated tab exports from shared UI packages. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
104 lines
3.2 KiB
TypeScript
104 lines
3.2 KiB
TypeScript
'use client'
|
|
|
|
import { useSearchParamsShallow } from 'common'
|
|
import { xor } from 'lodash-es'
|
|
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
|
|
|
const LOCAL_STORAGE_KEY = 'supabase.ui-patterns.ComplexTabs.withQueryParams.v0'
|
|
|
|
export interface UseTabsWithQueryParamsOptions {
|
|
tabIds: string[]
|
|
queryGroup?: string
|
|
}
|
|
|
|
/**
|
|
* Wraps the basic `Tabs` component from the `ui` library so it stores
|
|
* selection state in query params.
|
|
*/
|
|
export const useTabsWithQueryParams = ({ tabIds, queryGroup }: UseTabsWithQueryParamsOptions) => {
|
|
// Store in ref to avoid stale data in later timeout
|
|
const tabIdsRef = useRef(tabIds)
|
|
tabIdsRef.current = tabIds
|
|
|
|
// Store in ref to avoid stale data in later timeout
|
|
const queryGroupRef = useRef(queryGroup)
|
|
queryGroupRef.current = queryGroup
|
|
|
|
const searchParams = useSearchParamsShallow()
|
|
const queryTabMaybe = queryGroupRef.current && searchParams.get(queryGroupRef.current)
|
|
const queryTab =
|
|
queryTabMaybe && tabIdsRef.current.includes(queryTabMaybe) ? queryTabMaybe : undefined
|
|
|
|
const onTabSelected = useCallback(
|
|
(id: string) => {
|
|
if (queryGroupRef.current) {
|
|
if (!searchParams.getAll('queryGroups').includes(queryGroupRef.current)) {
|
|
searchParams.append('queryGroups', queryGroupRef.current)
|
|
}
|
|
searchParams.set(queryGroupRef.current, id)
|
|
}
|
|
},
|
|
[searchParams]
|
|
)
|
|
|
|
const checkedLocalStorage = useRef(false)
|
|
useEffect(() => {
|
|
let timeout: number | undefined
|
|
if (!checkedLocalStorage.current) {
|
|
// Timeout to avoid something (I think the router) overwriting it
|
|
timeout = window.setTimeout(() => {
|
|
if (
|
|
queryGroupRef.current &&
|
|
!new URLSearchParams(window.location.search).has(queryGroupRef.current)
|
|
) {
|
|
try {
|
|
const storedValues = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) ?? '')
|
|
if (storedValues === null || typeof storedValues !== 'object') return
|
|
|
|
let storedValue: any = null
|
|
let maxDiff = tabIdsRef.current.length
|
|
Object.entries(storedValues).forEach(([key, value]) => {
|
|
const arr = key.split(',')
|
|
const diff = xor(arr, tabIdsRef.current)
|
|
if (diff.length < maxDiff) {
|
|
maxDiff = diff.length
|
|
storedValue = value
|
|
}
|
|
})
|
|
|
|
if (storedValue && tabIdsRef.current.includes(storedValue)) {
|
|
onTabSelected(storedValue)
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
}, 300)
|
|
|
|
checkedLocalStorage.current = true
|
|
}
|
|
|
|
if (queryGroupRef.current && queryTab) {
|
|
let updatedValues: Record<string, unknown> = {}
|
|
try {
|
|
const oldValues = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) ?? '')
|
|
if (oldValues && typeof oldValues === 'object') {
|
|
updatedValues = oldValues
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
|
|
updatedValues[tabIdsRef.current.sort().join(',')] = queryTab
|
|
|
|
try {
|
|
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(updatedValues))
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
}, [queryTab, onTabSelected])
|
|
|
|
return useMemo(() => ({ queryTab, onTabSelected }), [queryTab, onTabSelected])
|
|
}
|