diff --git a/apps/docs/app/api/crawlers/route.ts b/apps/docs/app/api/crawlers/route.ts index 253e6648cb1..a6df01a9c3a 100644 --- a/apps/docs/app/api/crawlers/route.ts +++ b/apps/docs/app/api/crawlers/route.ts @@ -29,8 +29,8 @@ export async function GET(request: Request) { slug = maybeVersion } - let section: AbbrevApiReferenceSection - let sectionsWithUrl: Array + let section: AbbrevApiReferenceSection | undefined + let sectionsWithUrl: Array = [] try { const flattenedSections = (await getFlattenedSections(lib, version)) ?? [] sectionsWithUrl = flattenedSections.map((section) => { diff --git a/apps/docs/app/contributing/ContributingToC.tsx b/apps/docs/app/contributing/ContributingToC.tsx index ea2a953f15f..a8046833750 100644 --- a/apps/docs/app/contributing/ContributingToC.tsx +++ b/apps/docs/app/contributing/ContributingToC.tsx @@ -22,9 +22,9 @@ export function ContributingToc({ className }: { className?: string }) { ...document.querySelectorAll('article.prose > h2,h3:not(#feedback-title)'), ] as Array const tocItems = headings - .filter((heading) => !!heading.id) + .filter((heading) => !!heading.id && heading.textContent) .map((heading) => ({ - label: heading.textContent.substring(0, heading.textContent.length - 1), // Remove ending `#` + label: heading.textContent!.substring(0, heading.textContent!.length - 1), // Remove ending `#` anchor: heading.id, })) setTocItems(tocItems) diff --git a/apps/docs/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils.ts b/apps/docs/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils.ts index 31b068fcf08..c79f8ac313b 100644 --- a/apps/docs/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils.ts +++ b/apps/docs/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils.ts @@ -83,6 +83,12 @@ export async function generateAiPromptMetadata(props: { params: Promise<{ slug: const { slug } = await props.params const prompt = await getAiPrompt(slug) + if (!prompt) { + return { + title: 'AI Prompt | Supabase Docs', + } + } + return { title: `AI Prompt: ${prompt.heading} | Supabase Docs`, } diff --git a/apps/docs/app/guides/getting-started/ai-prompts/[slug]/page.tsx b/apps/docs/app/guides/getting-started/ai-prompts/[slug]/page.tsx index 864f46d17b7..055a4231d5a 100644 --- a/apps/docs/app/guides/getting-started/ai-prompts/[slug]/page.tsx +++ b/apps/docs/app/guides/getting-started/ai-prompts/[slug]/page.tsx @@ -1,3 +1,4 @@ +import { notFound } from 'next/navigation' import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' import { generateAiPromptMetadata, @@ -12,7 +13,12 @@ export default async function AiPromptsPage(props: { params: Promise<{ slug: str const { slug } = params - let { heading, content } = await getAiPrompt(slug) + const prompt = await getAiPrompt(slug) + if (!prompt) { + notFound() + } + + let { heading, content } = prompt content = ` ## How to use diff --git a/apps/docs/app/page.tsx b/apps/docs/app/page.tsx index 9286d1a2f20..f6d2be258b8 100644 --- a/apps/docs/app/page.tsx +++ b/apps/docs/app/page.tsx @@ -15,8 +15,12 @@ const generateMetadata = async (_, parent: ResolvingMetadata): Promise return { alternates: { - ...parentAlternates, canonical: `${BASE_PATH}`, + ...(parentAlternates && { + languages: parentAlternates.languages || undefined, + media: parentAlternates.media || undefined, + types: parentAlternates.types || undefined, + }), }, } } @@ -251,15 +255,17 @@ const HomePage = () => (
    - {MIGRATION_PAGES.sort((a, b) => a.name.localeCompare(b.name)).map((guide) => { - return ( -
  • - - - -
  • - ) - })} + {MIGRATION_PAGES.sort((a, b) => (a.name || '').localeCompare(b.name || '')).map( + (guide) => { + return ( +
  • + + + +
  • + ) + } + )}
diff --git a/apps/docs/components/ApiSchema.tsx b/apps/docs/components/ApiSchema.tsx index 952d60951f6..020bb8ae6a0 100644 --- a/apps/docs/components/ApiSchema.tsx +++ b/apps/docs/components/ApiSchema.tsx @@ -6,11 +6,11 @@ import { Tabs, TabPanel } from '~/features/ui/Tabs' type IParamProps = any const ApiSchema = ({ schema, id }: IParamProps) => { - let example: string + let example: string | undefined try { example = sample(schema, { skipReadOnly: true, quiet: true }) } catch { - // ignore + example = undefined } return ( { { - setFile({ file: e.target.files[0] }) + setFile({ file: e.target.files?.[0] || null }) }} /> @@ -156,7 +156,7 @@ const AppleSecretGenerator = () => { keyID, teamID, serviceID, - file.file + file.file! ) setKeyID(kid) setSecretKey(jwt) diff --git a/apps/docs/components/Breadcrumbs.tsx b/apps/docs/components/Breadcrumbs.tsx index d41b2b0e941..5eaee5cb0ac 100644 --- a/apps/docs/components/Breadcrumbs.tsx +++ b/apps/docs/components/Breadcrumbs.tsx @@ -181,7 +181,7 @@ function useBreadcrumbs() { return findMenuItemByUrl(menu, pathname, []) } -function findMenuItemByUrl(menu, targetUrl, parents = []) { +function findMenuItemByUrl(menu: any, targetUrl: string, parents: any[] = []) { // If the menu has items, recursively search through them if (menu.items) { for (let item of menu.items) { diff --git a/apps/docs/components/Extensions/Extensions.tsx b/apps/docs/components/Extensions/Extensions.tsx index b2228bb9d74..c9a3c201d5e 100644 --- a/apps/docs/components/Extensions/Extensions.tsx +++ b/apps/docs/components/Extensions/Extensions.tsx @@ -24,7 +24,7 @@ function getLinkTarget(link: string): LinkTarget { } function getUniqueTags(json: Extension[]): string[] { - const tags = [] + const tags: string[] = [] for (const item of json) { if (item.tags) { tags.push(...item.tags) diff --git a/apps/docs/components/Feedback/Feedback.tsx b/apps/docs/components/Feedback/Feedback.tsx index e2aa841aa43..e96f1907158 100644 --- a/apps/docs/components/Feedback/Feedback.tsx +++ b/apps/docs/components/Feedback/Feedback.tsx @@ -96,6 +96,8 @@ function Feedback({ className }: { className?: string }) { const showNo = unanswered || isNo async function sendFeedbackVote(response: Response) { + if (!supabase) return + const { error } = await supabase.from('feedback').insert({ vote: response, page: pathname, diff --git a/apps/docs/components/JwtGenerator/JwtGenerator.tsx b/apps/docs/components/JwtGenerator/JwtGenerator.tsx index f7e5132550a..e004b3c20ec 100644 --- a/apps/docs/components/JwtGenerator/JwtGenerator.tsx +++ b/apps/docs/components/JwtGenerator/JwtGenerator.tsx @@ -59,7 +59,7 @@ export default function JwtGenerator() { const [jwtSecret, setJwtSecret] = useState(secret) const [token, setToken] = useState(anonToken) const [signedToken, setSignedToken] = useState('') - const [err, setErr] = useState(null) + const [err, setErr] = useState('') const handleKeySelection = (e: ChangeEvent) => { const val = e.target.value @@ -71,7 +71,7 @@ export default function JwtGenerator() { try { const newTok = JSON.parse(e.target.value) setToken(newTok) - setErr(null) + setErr('') } catch (err) { const errMessage = !!err && typeof err === 'object' && 'message' in err && typeof err.message === 'string' diff --git a/apps/docs/components/MultiSelect.client.tsx b/apps/docs/components/MultiSelect.client.tsx deleted file mode 100644 index a56994cab30..00000000000 --- a/apps/docs/components/MultiSelect.client.tsx +++ /dev/null @@ -1,196 +0,0 @@ -'use client' - -import { ChevronsUpDown } from 'lucide-react' -import { - createContext, - forwardRef, - useCallback, - useContext, - useId, - useState, - PropsWithChildren, - useRef, - useEffect, -} from 'react' - -import { - cn, - Badge, - Checkbox_Shadcn_ as Checkbox, - Popover_Shadcn_ as Popover, - PopoverContent_Shadcn_ as PopoverContent, - PopoverTrigger_Shadcn_ as PopoverTrigger, -} from 'ui' - -interface Item { - value: string - label: string -} - -const MultiSelectContext = createContext<{ - selected: Item[] - handleSelect: (item: Item) => void - setOpen: (open: boolean) => void -}>({ - selected: [], - handleSelect: () => {}, - setOpen: () => {}, -}) - -function MultiSelectProvider({ - selected, - onSelectedChange, - setOpen, - children, -}: PropsWithChildren<{ - selected: Item[] - onSelectedChange: (selected: Item[] | ((selected: Item[]) => Item[])) => void - setOpen: (open: boolean) => void -}>) { - const handleSelect = useCallback( - (item: Item) => { - onSelectedChange((current: Item[]) => { - const isSelected = current.some((currItem) => currItem.value === item.value) - if (isSelected) { - return current.filter((currItem) => currItem.value !== item.value) - } else { - return [...current, item] - } - }) - }, - [onSelectedChange] - ) - - return ( - - {children} - - ) -} - -function useMultiSelect() { - const context = useContext(MultiSelectContext) - if (!context) { - throw new Error('useMultiSelect must be used within a MultiSelectProvider') - } - return context -} - -export function MultiSelect({ - open: _controlledOpen, - setOpen: _setControlledOpen, - selected, - onSelectedChange, - children, - ...props -}: PropsWithChildren< - { - open?: boolean - setOpen?: React.Dispatch> - selected?: Item[] - onSelectedChange: (selected: Item[] | ((selected: Item[]) => Item[])) => void - } & React.ComponentProps ->) { - const [_internalOpen, _setInternalOpen] = useState(false) - const open = _controlledOpen ?? _internalOpen - const setOpen = _setControlledOpen ?? _setInternalOpen - - return ( - - - {children} - - - ) -} - -const Trigger = forwardRef< - HTMLButtonElement, - { label?: string; className?: string } & React.ComponentProps ->(({ label, className, ...props }, ref) => { - const { selected } = useMultiSelect() - - const [measuredWidth, setMeasuredWidth] = useState(0) - const measuredRef = (node: HTMLElement) => { - if (node) { - setMeasuredWidth(node.getBoundingClientRect().width) - } - } - - const badgeWidth = 100 // Approximate width of a badge in pixels - const maxBadges = Math.max(1, Math.floor((measuredWidth || 200) / badgeWidth)) - const visibleBadges = selected.slice(0, maxBadges) - const extraBadgesCount = Math.max(0, selected.length - maxBadges) - - const badgeClasses = 'bg-200' - - return ( - - - - ) -}) -Trigger.displayName = 'Trigger' -MultiSelect.Trigger = Trigger - -const Content = forwardRef< - HTMLDivElement, - PropsWithChildren<{ className?: string } & React.ComponentProps> ->(({ children, className, ...props }, ref) => { - return ( - - {children} - - ) -}) -Content.displayName = 'Content' -MultiSelect.Content = Content - -export function Item({ item, className }: { item: Item; className?: string }) { - const id = useId() - const { selected, handleSelect } = useMultiSelect() - - return ( -
- sel.value === item.value)} - onCheckedChange={() => handleSelect(item)} - /> - -
- ) -} -MultiSelect.Item = Item diff --git a/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx index f34c38f6830..5f57d3c6d74 100644 --- a/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx @@ -71,7 +71,7 @@ const AccordionMenuItem = ({ section }: { section: DropdownMenuItem[] }) => { ) : ( {section[0].label} diff --git a/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx index 3adebfe9d72..ce620239cf0 100644 --- a/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx @@ -81,7 +81,7 @@ const GlobalNavigationMenu: FC = () => { )} > {section[0].label === 'Home' ? ( - + ) : ( section[0].label )} @@ -125,7 +125,7 @@ const GlobalNavigationMenu: FC = () => { > { )} > {section[0].label === 'Home' ? ( - + ) : ( section[0].label )} diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.Context.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.Context.tsx index dd26ebb55c4..6de03119722 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.Context.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.Context.tsx @@ -3,7 +3,7 @@ import { createContext, useContext } from 'react' interface ContextProps { - activeRefItem: string + activeRefItem: string | undefined setActiveRefItem: (x: string) => void } @@ -15,7 +15,7 @@ interface Provider extends ContextProps { // createContext matches the shape that the consumers expect! const NavMenuContext = createContext({ activeRefItem: undefined, - setActiveRefItem: undefined, + setActiveRefItem: () => {}, }) export const NavigationMenuContextProvider = (props: Provider) => { diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx index d33b0c9a105..7e9056aad82 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx @@ -1,9 +1,9 @@ import { memo } from 'react' import type { NavMenuSection } from '../Navigation.types' +import { useCloseMenuOnRouteChange } from './NavigationMenu.utils' import NavigationMenuGuideList from './NavigationMenuGuideList' import NavigationMenuRefList from './NavigationMenuRefList' -import { useCloseMenuOnRouteChange } from './NavigationMenu.utils' enum MenuId { GettingStarted = 'gettingstarted', @@ -251,6 +251,8 @@ function getMenuById(id: MenuId) { } function getMenuElement(menu: Menu | undefined, props?: any) { + if (!menu) return null + const menuType = menu?.type switch (menuType) { case 'guide': @@ -260,7 +262,7 @@ function getMenuElement(menu: Menu | undefined, props?: any) { ) default: @@ -283,5 +285,5 @@ const NavigationMenu = ({ return getMenuElement(menu, { additionalNavItems }) } -export { MenuId, getMenuById } +export { getMenuById, MenuId } export default memo(NavigationMenu) diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.utils.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.utils.ts index d8cc3c0b782..4f50c7ee18c 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.utils.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.utils.ts @@ -1,7 +1,7 @@ 'use client' -import { useEffect, useState } from 'react' import { usePathname } from 'next/navigation' +import { useEffect, useState } from 'react' import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu' import type { ICommonItem } from '~/components/reference/Reference.types' import type { Json } from '~/features/helpers.types' @@ -52,7 +52,10 @@ export function deepFilterSections( * * See https://webpack.js.org/api/module-methods/#dynamic-expressions-in-import */ -export function useCommonSections(commonSectionsFile: string) { +export function useCommonSections( + commonSectionsFile: string, + { enabled = true }: { enabled: boolean } +) { const [commonSections, setCommonSections] = useState() useEffect(() => { @@ -67,6 +70,10 @@ export function useCommonSections(commonSectionsFile: string) { fetchCommonSections() }, [commonSectionsFile]) + if (!enabled) { + return null + } + return commonSections } diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefList.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefList.tsx index 9725daa229b..c760bc10b51 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefList.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefList.tsx @@ -16,7 +16,7 @@ const NavigationMenuRefList = ({ commonSectionsFile, specFile, }: NavigationMenuRefListProps) => { - const commonSections = useCommonSections(commonSectionsFile) + const commonSections = useCommonSections(commonSectionsFile, { enabled: !!commonSectionsFile }) const spec = useSpec(specFile) if (!commonSections) { diff --git a/apps/docs/components/Navigation/SideBar.tsx b/apps/docs/components/Navigation/SideBar.tsx index 7e02c14665f..d754fadbc7a 100644 --- a/apps/docs/components/Navigation/SideBar.tsx +++ b/apps/docs/components/Navigation/SideBar.tsx @@ -28,7 +28,7 @@ const SideBar = ({ menuItems = [] }: { menuItems: any }) => { if (foundItem) return group }) - const currentSubSection: NavMenuSection = + const currentSubSection: NavMenuSection | undefined = currentSection !== undefined ? currentSection.items.find((section) => { if (section.items.length === 0) { @@ -113,7 +113,7 @@ const SideBar = ({ menuItems = [] }: { menuItems: any }) => { {group.items.map((section: NavMenuSection) => { if (section.items.length === 0) { return ( - +
{ {section.items.map((item: NavMenuSection) => ( - +
stateSummary !== 'loggedIn.dataSuccess.hasData' ? [] - : projects.map((project) => { - const organization = organizations.find((org) => org.id === project.organization_id) - return { - id: project.ref, - value: toOrgProjectValue(organization, project), - displayName: toDisplayNameOrgProject(organization, project), - } - }), + : (projects! + .map((project) => { + const organization = organizations!.find((org) => org.id === project.organization_id)! + return { + id: project.ref, + value: toOrgProjectValue(organization, project), + displayName: toDisplayNameOrgProject(organization, project), + } + }) + .filter(Boolean) as ComboBoxOption[]), [organizations, projects, stateSummary] ) @@ -138,18 +140,18 @@ function OrgProjectSelector() { const storedMaybeOrgId = retrieve('local', LOCAL_STORAGE_KEYS.SAVED_ORG) const storedMaybeProjectRef = retrieve('local', LOCAL_STORAGE_KEYS.SAVED_PROJECT) - let storedOrg: Org - let storedProject: Project + let storedOrg: Org | undefined + let storedProject: Project | undefined if (storedMaybeOrgId && storedMaybeProjectRef) { - storedOrg = organizations.find((org) => org.id === Number(storedMaybeOrgId)) - storedProject = projects.find((project) => project.ref === storedMaybeProjectRef) + storedOrg = organizations!.find((org) => org.id === Number(storedMaybeOrgId)) + storedProject = projects!.find((project) => project.ref === storedMaybeProjectRef) } if (storedOrg && storedProject && storedProject.organization_id === storedOrg.id) { setSelectedOrgProject(storedOrg, storedProject) - } else { - const firstProject = projects[0] - const matchingOrg = organizations.find((org) => org.id === firstProject.organization_id) + } else if (projects!.length > 0) { + const firstProject = projects![0] + const matchingOrg = organizations!.find((org) => org.id === firstProject.organization_id) if (matchingOrg) setSelectedOrgProject(matchingOrg, firstProject) } } @@ -172,8 +174,8 @@ function OrgProjectSelector() { const [orgId, projectRef] = fromOrgProjectValue(optionValue) if (!orgId || !projectRef) return - const org = organizations.find((org) => org.id === orgId) - const project = projects.find((project) => project.ref === projectRef) + const org = organizations?.find((org) => org.id === orgId) + const project = projects?.find((project) => project.ref === projectRef) if (org && project && project.organization_id === org.id) { setSelectedOrgProject(org, project) @@ -214,7 +216,7 @@ function BranchSelector() { const formattedData: ComboBoxOption[] = stateSummary !== 'loggedIn.branches.dataSuccess.hasData' ? [] - : data.map((branch) => ({ + : data!.map((branch) => ({ id: branch.id, displayName: branch.name, value: toBranchValue(branch), @@ -224,18 +226,18 @@ function BranchSelector() { if (stateSummary === 'loggedIn.branches.dataSuccess.hasData' && !selectedBranch) { const storedMaybeBranchId = retrieve('local', LOCAL_STORAGE_KEYS.SAVED_BRANCH) - let storedBranch: Branch + let storedBranch: Branch | undefined if (storedMaybeBranchId) { - storedBranch = data.find((branch) => branch.id === storedMaybeBranchId) + storedBranch = data!.find((branch) => branch.id === storedMaybeBranchId) } if (storedBranch) { setSelectedBranch(storedBranch) } else { - const productionBranch = data.find( + const productionBranch = data!.find( (branch) => branch.project_ref === branch.parent_project_ref ) - setSelectedBranch(productionBranch ?? data[0]) + setSelectedBranch(productionBranch ?? data![0]) } } }, [data, selectedBranch, setSelectedBranch, stateSummary]) @@ -255,7 +257,7 @@ function BranchSelector() { onSelectOption={(option) => { const [branchId] = fromBranchValue(option) if (branchId) { - const branch = data.find((branch) => branch.id === branchId) + const branch = data?.find((branch) => branch.id === branchId) if (branch) setSelectedBranch(branch) } }} @@ -322,23 +324,23 @@ function VariableView({ variable, className }: { variable: Variable; className?: ? 'loggedIn.selectedProject.dataPending' : ( needsApiQuery - ? isApiError || isInvalidApiData(apiData) - : isSupavisorError || isInvalidSupavisorData(supavisorConfig) + ? isApiError || isInvalidApiData(apiData!) + : isSupavisorError || isInvalidSupavisorData(supavisorConfig!) ) ? 'loggedIn.selectedProject.dataError' : 'loggedIn.selectedProject.dataSuccess' - let variableValue: string = null + let variableValue: string = '' if (stateSummary === 'loggedIn.selectedProject.dataSuccess') { switch (variable) { case 'url': - variableValue = `https://${apiData.app_config!.endpoint}` + variableValue = `https://${apiData?.app_config?.endpoint}` break case 'anonKey': - variableValue = apiData.service_api_keys!.find((key) => key.tags === 'anon')!.api_key + variableValue = apiData?.service_api_keys?.find((key) => key.tags === 'anon')?.api_key || '' break case 'sessionPooler': - variableValue = supavisorConfig[0].connection_string + variableValue = supavisorConfig?.[0]?.connection_string || '' } } diff --git a/apps/docs/components/RealtimeLimitsEstimator/RealtimeLimitsEstimator.tsx b/apps/docs/components/RealtimeLimitsEstimator/RealtimeLimitsEstimator.tsx index 97787e0cfee..de07c73bdf0 100644 --- a/apps/docs/components/RealtimeLimitsEstimator/RealtimeLimitsEstimator.tsx +++ b/apps/docs/components/RealtimeLimitsEstimator/RealtimeLimitsEstimator.tsx @@ -137,7 +137,11 @@ export default function RealtimeLimitsEstimater({}) {