diff --git a/apps/studio/components/interfaces/Account/Preferences/AccountIdentities.tsx b/apps/studio/components/interfaces/Account/Preferences/AccountIdentities.tsx index 588636ebcbe6c..eb40c22ec1787 100644 --- a/apps/studio/components/interfaces/Account/Preferences/AccountIdentities.tsx +++ b/apps/studio/components/interfaces/Account/Preferences/AccountIdentities.tsx @@ -140,7 +140,7 @@ export const AccountIdentities = () => {
{provider === 'email' && ( )} { - if (failureCount >= 2) return false + retry: (failureCount: number, error) => { + if (error.code === 404 || failureCount >= 2) return false return true }, } @@ -151,18 +151,16 @@ export const ReportBlock = ({ ? String(executeSqlError) : undefined } - isExecuting={executeSqlLoading} + isExecuting={!contentError && executeSqlLoading} isWriteQuery={isWriteQuery} actions={ - !isLoadingContent && ( - } - className="w-7 h-7" - onClick={() => onRemoveChart({ metric: { key: item.attribute } })} - tooltip={{ content: { side: 'bottom', text: 'Remove chart' } }} - /> - ) + } + className="w-7 h-7" + onClick={() => onRemoveChart({ metric: { key: item.attribute } })} + tooltip={{ content: { side: 'bottom', text: 'Remove chart' } }} + /> } onExecute={(queryType) => { refetch() diff --git a/apps/studio/components/interfaces/SignIn/ForgotPasswordWizard.tsx b/apps/studio/components/interfaces/SignIn/ForgotPasswordWizard.tsx index 430ee981c9d85..332659f46d06d 100644 --- a/apps/studio/components/interfaces/SignIn/ForgotPasswordWizard.tsx +++ b/apps/studio/components/interfaces/SignIn/ForgotPasswordWizard.tsx @@ -1,17 +1,16 @@ import HCaptcha from '@hcaptcha/react-hcaptcha' import { zodResolver } from '@hookform/resolvers/zod' +import { useResetPasswordMutation } from 'data/misc/reset-password-mutation' +import { BASE_PATH } from 'lib/constants' +import { auth } from 'lib/gotrue' import { useRouter } from 'next/router' import { useRef, useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' -import * as z from 'zod' - -import { useResetPasswordMutation } from 'data/misc/reset-password-mutation' -import { BASE_PATH } from 'lib/constants' -import { auth } from 'lib/gotrue' import { Button, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, Input_Shadcn_ } from 'ui' import { Admonition } from 'ui-patterns' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import * as z from 'zod' const forgotPasswordSchema = z.object({ email: z.string().min(1, 'Please provide an email address').email('Must be a valid email'), diff --git a/apps/studio/components/interfaces/SignIn/ResetPasswordForm.tsx b/apps/studio/components/interfaces/SignIn/ResetPasswordForm.tsx index 2d2955982c7eb..7d85034b5aff8 100644 --- a/apps/studio/components/interfaces/SignIn/ResetPasswordForm.tsx +++ b/apps/studio/components/interfaces/SignIn/ResetPasswordForm.tsx @@ -1,61 +1,66 @@ import { zodResolver } from '@hookform/resolvers/zod' +import { useParams } from 'common' +import { captureCriticalError } from 'lib/error-reporting' +import { auth, getReturnToPath } from 'lib/gotrue' import { Eye, EyeOff } from 'lucide-react' import { useRouter } from 'next/router' import { useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' +import { Button, cn, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, Separator } from 'ui' +import { Input } from 'ui-patterns/DataInputs/Input' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { z } from 'zod' -import { captureCriticalError } from 'lib/error-reporting' -import { auth, getReturnToPath } from 'lib/gotrue' -import { - Button, - Form_Shadcn_, - FormControl_Shadcn_, - FormField_Shadcn_, - FormItem_Shadcn_, - FormMessage_Shadcn_, - Input, -} from 'ui' - import PasswordConditionsHelper from './PasswordConditionsHelper' -// Convert the existing yup passwordSchema to Zod +const passwordValidation = z + .string() + .min(1, 'Password is required') + .max(72, 'Password cannot exceed 72 characters') + .refine((password) => { + const hasUppercase = /[A-Z]/.test(password) + const hasLowercase = /[a-z]/.test(password) + const hasNumber = /[0-9]/.test(password) + const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};`':"\\|,.<>\/?]/.test(password) + const isLongEnough = password.length >= 8 + + return hasUppercase && hasLowercase && hasNumber && hasSpecialChar && isLongEnough + }, 'Password must contain at least 8 characters, including uppercase, lowercase, number, and special character') + const passwordSchema = z.object({ - password: z - .string() - .min(1, 'Password is required') - .max(72, 'Password cannot exceed 72 characters') - .refine((password) => { - // Basic password validation - you can enhance this based on your requirements - const hasUppercase = /[A-Z]/.test(password) - const hasLowercase = /[a-z]/.test(password) - const hasNumber = /[0-9]/.test(password) - const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};`':"\\|,.<>\/?]/.test(password) - const isLongEnough = password.length >= 8 - - return hasUppercase && hasLowercase && hasNumber && hasSpecialChar && isLongEnough - }, 'Password must contain at least 8 characters, including uppercase, lowercase, number, and special character'), + currentPassword: z.string().min(1, 'Current password is required'), + password: passwordValidation, +}) + +const recoveryPasswordSchema = z.object({ + currentPassword: z.string().optional(), + password: passwordValidation, }) type FormData = z.infer -const ResetPasswordForm = () => { +export const ResetPasswordForm = () => { const router = useRouter() + const { type } = useParams() + const requireCurrentPassword = type === 'change' + const [showConditions, setShowConditions] = useState(false) const [passwordHidden, setPasswordHidden] = useState(true) + const [currentPasswordHidden, setCurrentPasswordHidden] = useState(true) const form = useForm({ - resolver: zodResolver(passwordSchema), - defaultValues: { - password: '', - }, + resolver: zodResolver(requireCurrentPassword ? passwordSchema : recoveryPasswordSchema), + defaultValues: { password: '', currentPassword: '' }, mode: 'onChange', }) const onResetPassword = async (data: FormData) => { const toastId = toast.loading('Saving password...') - const { error } = await auth.updateUser({ password: data.password }) + const { error } = await auth.updateUser({ + password: data.password, + ...(requireCurrentPassword ? { current_password: data.currentPassword } : {}), + }) if (!error) { toast.success('Password saved successfully!', { id: toastId }) @@ -72,16 +77,46 @@ const ResetPasswordForm = () => { return (
+ {requireCurrentPassword && ( + ( + + + : } + type="default" + className="w-7" + onClick={() => setCurrentPasswordHidden((prev) => !prev)} + /> + } + {...field} + onBlur={() => { + field.onBlur() + setCurrentPasswordHidden(true) + }} + /> + + + )} + /> + )} ( - + setShowConditions(true)} @@ -90,27 +125,31 @@ const ResetPasswordForm = () => { - -
- - - - - ) -} diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPanel/HelpButton.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPanel/HelpButton.tsx new file mode 100644 index 0000000000000..c986e14a34c83 --- /dev/null +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPanel/HelpButton.tsx @@ -0,0 +1,46 @@ +import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' +import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { HelpCircle } from 'lucide-react' +import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' +import { cn } from 'ui' + +export const HelpButton = () => { + const { toggleSidebar, activeSidebar } = useSidebarManagerSnapshot() + const { data: project } = useSelectedProjectQuery() + const { data: org } = useSelectedOrganizationQuery() + const { mutate: sendEvent } = useSendEventMutation() + + const isOpen = activeSidebar?.id === SIDEBAR_KEYS.HELP_PANEL + + return ( + { + toggleSidebar(SIDEBAR_KEYS.HELP_PANEL) + // Don't send telemetry event if dropdown is already open + if (!isOpen) { + sendEvent({ + action: 'help_button_clicked', + groups: { project: project?.ref, organization: org?.slug }, + }) + } + }} + tooltip={{ content: { side: 'bottom', text: 'Help' } }} + > + + + ) +} diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpDropdown/HelpOptionsList.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPanel/HelpOptionsList.tsx similarity index 97% rename from apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpDropdown/HelpOptionsList.tsx rename to apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPanel/HelpOptionsList.tsx index 33171c30f4444..278f35584623b 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpDropdown/HelpOptionsList.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPanel/HelpOptionsList.tsx @@ -6,8 +6,8 @@ import { useRouter } from 'next/router' import SVG from 'react-inlinesvg' import { AiIconAnimation, ButtonGroup, ButtonGroupItem } from 'ui' -import type { HelpOptionId } from './HelpDropdown.constants' -import { HELP_OPTION_IDS } from './HelpDropdown.constants' +import type { HelpOptionId } from './HelpPanel.constants' +import { HELP_OPTION_IDS } from './HelpPanel.constants' const DISCORD_URL = 'https://discord.supabase.com' const STATUS_URL = 'https://status.supabase.com' diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpDropdown/HelpDropdown.constants.ts b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPanel/HelpPanel.constants.ts similarity index 100% rename from apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpDropdown/HelpDropdown.constants.ts rename to apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPanel/HelpPanel.constants.ts diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPanel/HelpPanel.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPanel/HelpPanel.tsx new file mode 100644 index 0000000000000..264450d190d89 --- /dev/null +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPanel/HelpPanel.tsx @@ -0,0 +1,97 @@ +import { IS_PLATFORM } from 'common' +import type { SupportFormUrlKeys } from 'components/interfaces/Support/SupportForm.utils' +import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { X } from 'lucide-react' +import Image from 'next/image' +import { useRouter } from 'next/router' +import SVG from 'react-inlinesvg' +import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' +import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' +import { Button, cn, Separator } from 'ui' +import styleHandler from 'ui/src/lib/theme/styleHandler' + +import { ASSISTANT_SUGGESTIONS } from './HelpPanel.constants' +import { HelpSection } from './HelpSection' + +export const HelpPanel = ({ + onClose, + projectRef, + supportLinkQueryParams, +}: { + onClose: () => void + projectRef: string | undefined + supportLinkQueryParams: Partial | undefined +}) => { + const snap = useAiAssistantStateSnapshot() + const { openSidebar, closeSidebar } = useSidebarManagerSnapshot() + const router = useRouter() + + const __styles = styleHandler('popover') + + return ( +
+
+ Help & Support + closeSidebar(SIDEBAR_KEYS.HELP_PANEL)} + icon={} + tooltip={{ content: { side: 'bottom', text: 'Close' } }} + /> +
+ { + onClose() + openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) + snap.newChat(ASSISTANT_SUGGESTIONS) + }} + onSupportClick={onClose} + /> + +
+
+
Community support
+

+ Our Discord community can help with code-related issues. Many questions are answered in + minutes. +

+
+ +
+
+ ) +} diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPanel/HelpPanel.utils.test.ts b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPanel/HelpPanel.utils.test.ts new file mode 100644 index 0000000000000..95e640f0e3846 --- /dev/null +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPanel/HelpPanel.utils.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest' + +import { getSupportLinkQueryParams } from './HelpPanel.utils' + +describe('getSupportLinkQueryParams', () => { + it('returns { projectRef } when project has parent_project_ref', () => { + expect( + getSupportLinkQueryParams( + { parent_project_ref: 'main-project' }, + { slug: 'my-org' }, + 'router-ref' + ) + ).toEqual({ projectRef: 'main-project' }) + }) + + it('returns { projectRef } from routerRef when project has no parent_project_ref', () => { + expect(getSupportLinkQueryParams({}, { slug: 'my-org' }, 'router-ref')).toEqual({ + projectRef: 'router-ref', + }) + }) + + it('returns { projectRef } from routerRef when project is undefined', () => { + expect(getSupportLinkQueryParams(undefined, { slug: 'my-org' }, 'router-ref')).toEqual({ + projectRef: 'router-ref', + }) + }) + + it('returns { orgSlug } when no projectRef (no project, no routerRef)', () => { + expect(getSupportLinkQueryParams(undefined, { slug: 'my-org' }, undefined)).toEqual({ + orgSlug: 'my-org', + }) + }) + + it('returns { orgSlug } when project and routerRef are undefined but org has slug', () => { + expect(getSupportLinkQueryParams(undefined, { slug: 'acme' }, undefined)).toEqual({ + orgSlug: 'acme', + }) + }) + + it('returns undefined when project, org and routerRef give no ref', () => { + expect(getSupportLinkQueryParams(undefined, undefined, undefined)).toBeUndefined() + }) + + it('returns undefined when org has no slug and no projectRef', () => { + expect(getSupportLinkQueryParams(undefined, {}, undefined)).toBeUndefined() + }) + + it('returns undefined when org is undefined and no projectRef', () => { + expect(getSupportLinkQueryParams(undefined, undefined, undefined)).toBeUndefined() + }) + + it('prefers parent_project_ref over routerRef when both are present', () => { + expect( + getSupportLinkQueryParams({ parent_project_ref: 'parent-ref' }, { slug: 'org' }, 'router-ref') + ).toEqual({ projectRef: 'parent-ref' }) + }) +}) diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpDropdown/HelpDropdown.utils.ts b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPanel/HelpPanel.utils.ts similarity index 100% rename from apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpDropdown/HelpDropdown.utils.ts rename to apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPanel/HelpPanel.utils.ts diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpDropdown/HelpSection.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPanel/HelpSection.tsx similarity index 95% rename from apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpDropdown/HelpSection.tsx rename to apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPanel/HelpSection.tsx index bc6a246880002..7aea2451dc4f3 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpDropdown/HelpSection.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/HelpPanel/HelpSection.tsx @@ -1,8 +1,8 @@ import type { SupportFormUrlKeys } from 'components/interfaces/Support/SupportForm.utils' import { cn } from 'ui' -import type { HelpOptionId } from './HelpDropdown.constants' import { HelpOptionsList } from './HelpOptionsList' +import type { HelpOptionId } from './HelpPanel.constants' type HelpSectionProps = { excludeIds?: HelpOptionId[] diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx index 08f9f317bf2d3..ce5d2e78aebc4 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx @@ -26,7 +26,7 @@ import { CommandMenuTriggerInput } from 'ui-patterns' import { BreadcrumbsView } from './BreadcrumbsView' import { FeedbackDropdown } from './FeedbackDropdown/FeedbackDropdown' -import { HelpDropdown } from './HelpDropdown/HelpDropdown' +import { HelpButton } from './HelpPanel/HelpButton' import { HomeIcon } from './HomeIcon' import { LocalVersionPopover } from './LocalVersionPopover' import { MergeRequestButton } from './MergeRequestButton' @@ -231,7 +231,7 @@ export const LayoutHeader = ({ '[&_.command-shortcut>div]:text-foreground-lighter' )} /> - + {!!projectRef && ( @@ -257,7 +257,7 @@ export const LayoutHeader = ({ [&_.command-shortcut>div]:text-foreground-lighter " /> - + {!!projectRef && ( diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider.tsx index f653c316d499d..74aa05af224b7 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider.tsx @@ -1,22 +1,37 @@ -import { useRouter } from 'next/router' -import { parseAsString, useQueryState } from 'nuqs' -import { PropsWithChildren, useEffect } from 'react' - import { LOCAL_STORAGE_KEYS } from 'common' -import { AdvisorPanel } from 'components/ui/AdvisorPanel/AdvisorPanel' -import { AIAssistant } from 'components/ui/AIAssistantPanel/AIAssistant' -import { EditorPanel } from 'components/ui/EditorPanel/EditorPanel' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import useLatest from 'hooks/misc/useLatest' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import dynamic from 'next/dynamic' +import { useRouter } from 'next/router' +import { parseAsString, useQueryState } from 'nuqs' +import { useEffect, type PropsWithChildren } from 'react' import { useRegisterSidebar, useSidebarManagerSnapshot } from 'state/sidebar-manager-state' +import { getSupportLinkQueryParams } from '../LayoutHeader/HelpPanel/HelpPanel.utils' + +const AdvisorPanel = dynamic(() => + import('components/ui/AdvisorPanel/AdvisorPanel').then((m) => m.AdvisorPanel) +) +const AIAssistant = dynamic(() => + import('components/ui/AIAssistantPanel/AIAssistant').then((m) => m.AIAssistant) +) +const EditorPanel = dynamic(() => + import('components/ui/EditorPanel/EditorPanel').then((m) => m.EditorPanel) +) +const HelpPanel = dynamic(() => + import('components/layouts/ProjectLayout/LayoutHeader/HelpPanel/HelpPanel').then( + (m) => m.HelpPanel + ) +) + export const SIDEBAR_KEYS = { AI_ASSISTANT: 'ai-assistant', EDITOR_PANEL: 'editor-panel', ADVISOR_PANEL: 'advisor-panel', + HELP_PANEL: 'help-panel', } as const export const LayoutSidebarProvider = ({ children }: PropsWithChildren) => { @@ -24,7 +39,7 @@ export const LayoutSidebarProvider = ({ children }: PropsWithChildren) => { const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() const { mutate: sendEvent } = useSendEventMutation() - const { openSidebar, activeSidebar } = useSidebarManagerSnapshot() + const { openSidebar, closeSidebar, activeSidebar } = useSidebarManagerSnapshot() const [sidebarURLParam, setSidebarUrlParam] = useQueryState('sidebar', parseAsString) const [sidebarLocalStorage, setSidebarLocalStorage, { isSuccess: isLoadedLocalStorage }] = @@ -36,6 +51,23 @@ export const LayoutSidebarProvider = ({ children }: PropsWithChildren) => { useRegisterSidebar(SIDEBAR_KEYS.AI_ASSISTANT, () => , {}, 'i', !!project) useRegisterSidebar(SIDEBAR_KEYS.EDITOR_PANEL, () => , {}, 'e', !!project) useRegisterSidebar(SIDEBAR_KEYS.ADVISOR_PANEL, () => , {}, undefined, true) + useRegisterSidebar( + SIDEBAR_KEYS.HELP_PANEL, + () => ( + closeSidebar(SIDEBAR_KEYS.HELP_PANEL)} + projectRef={project?.ref} + supportLinkQueryParams={getSupportLinkQueryParams( + project, + org, + router.query.ref as string | undefined + )} + /> + ), + {}, + undefined, + true + ) useEffect(() => { if (!!project) { diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/index.test.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/index.test.tsx index 889e07bc12443..8872548186be2 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/index.test.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/index.test.tsx @@ -1,10 +1,11 @@ import { act, screen, waitFor } from '@testing-library/react' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - import { sidebarManagerState } from 'state/sidebar-manager-state' import { render } from 'tests/helpers' import { routerMock } from 'tests/lib/route-mock' import { ResizablePanel, ResizablePanelGroup } from 'ui' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { MobileSheetProvider } from '../NavigationBar/MobileSheetContext' import { LayoutSidebar } from './index' import { LayoutSidebarProvider, SIDEBAR_KEYS } from './LayoutSidebarProvider' @@ -109,7 +110,9 @@ describe('LayoutSidebar', () => {
- + + + ) diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/index.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/index.tsx index 3e3161f0ce447..ac76bf481647d 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/index.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/index.tsx @@ -1,5 +1,10 @@ +import { useBreakpoint } from 'common' +import { useEffect } from 'react' import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' -import { ResizableHandle, ResizablePanel, cn } from 'ui' +import { cn, ResizableHandle, ResizablePanel } from 'ui' +import { MobileSheetNav } from 'ui-patterns' + +import { useMobileSheet } from '../NavigationBar/MobileSheetContext' // Having these params as props as otherwise it's quite hard to visually check the sizes in DefaultLayout // as react resizeable panels requires all these values to be valid to render correctly @@ -14,10 +19,38 @@ export const LayoutSidebar = ({ maxSize = '50', defaultSize = '30', }: LayoutSidebarProps) => { - const { activeSidebar } = useSidebarManagerSnapshot() + const { activeSidebar, closeActive } = useSidebarManagerSnapshot() + const isMobile = useBreakpoint('md') + const { content: mobileSheetContent, setContent: setMobileSheetContent } = useMobileSheet() + + // On mobile the sidebar content is rendered in MobileSheetNav + useEffect(() => { + if (isMobile && activeSidebar?.component) { + setMobileSheetContent(activeSidebar.id) + } else { + setMobileSheetContent(null) + } + }, [isMobile, activeSidebar, setMobileSheetContent]) if (!activeSidebar?.component) return null + if (isMobile) + return ( + { + if (!open) { + setMobileSheetContent(null) + closeActive() + } + }} + > + {activeSidebar?.component?.()} + + ) + return ( <> diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/MobileNavigationBar.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/MobileNavigationBar.tsx index 04b87035fd372..15873138927b0 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/MobileNavigationBar.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/MobileNavigationBar.tsx @@ -1,14 +1,12 @@ +import { useParams } from 'common' +import { SidebarContent } from 'components/interfaces/Sidebar' +import { IS_PLATFORM } from 'lib/constants' import { Menu, Search } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useState } from 'react' - -import { useParams } from 'common' -import { SidebarContent } from 'components/interfaces/Sidebar' -import { IS_PLATFORM } from 'lib/constants' import { Button, cn } from 'ui' -import { CommandMenuTrigger } from 'ui-patterns' -import MobileSheetNav from 'ui-patterns/MobileSheetNav/MobileSheetNav' +import { CommandMenuTrigger, MobileSheetNav } from 'ui-patterns' export const ICON_SIZE = 20 export const ICON_STROKE_WIDTH = 1.5 diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/MobileSheetContext.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/MobileSheetContext.tsx new file mode 100644 index 0000000000000..72a6f76ff5059 --- /dev/null +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/MobileSheetContext.tsx @@ -0,0 +1,33 @@ +import type { PropsWithChildren } from 'react' +import { createContext, useCallback, useContext, useState } from 'react' + +export type MobileSheetContentType = null | string + +type MobileSheetContextValue = { + content: MobileSheetContentType + setContent: (content: MobileSheetContentType) => void +} + +const MobileSheetContext = createContext(null) + +export function MobileSheetProvider({ children }: PropsWithChildren) { + const [content, setContentState] = useState(null) + + const setContent = useCallback((next: MobileSheetContentType) => { + setContentState(next) + }, []) + + return ( + + {children} + + ) +} + +export function useMobileSheet(): MobileSheetContextValue { + const ctx = useContext(MobileSheetContext) + if (!ctx) { + throw new Error('useMobileSheet must be used within MobileSheetProvider') + } + return ctx +} diff --git a/apps/studio/components/ui/QueryBlock/QueryBlock.tsx b/apps/studio/components/ui/QueryBlock/QueryBlock.tsx index c8a4bf9a38c0a..d6d530ebbe37d 100644 --- a/apps/studio/components/ui/QueryBlock/QueryBlock.tsx +++ b/apps/studio/components/ui/QueryBlock/QueryBlock.tsx @@ -146,59 +146,62 @@ export const QueryBlock = ({ label={label} badge={isWriteQuery && Write} actions={ - disabled ? null : ( - <> - } - onClick={() => setShowSql(!showSql)} - tooltip={{ - content: { side: 'bottom', text: showSql ? 'Hide query' : 'Show query' }, - }} - /> - {hasResults && ( - { - if (onUpdateChartConfig) onUpdateChartConfig({ chartConfig: { view: nextView } }) - setChartSettings({ ...chartSettings, view: nextView }) - }} - updateChartConfig={(config) => { - if (onUpdateChartConfig) onUpdateChartConfig({ chartConfig: config }) - setChartSettings(config) + <> + {!disabled && ( + <> + } + onClick={() => setShowSql(!showSql)} + tooltip={{ + content: { side: 'bottom', text: showSql ? 'Hide query' : 'Show query' }, }} /> - )} + {hasResults && ( + { + if (onUpdateChartConfig) + onUpdateChartConfig({ chartConfig: { view: nextView } }) + setChartSettings({ ...chartSettings, view: nextView }) + }} + updateChartConfig={(config) => { + if (onUpdateChartConfig) onUpdateChartConfig({ chartConfig: config }) + setChartSettings(config) + }} + /> + )} - - } - loading={isExecuting} - disabled={isExecuting || disabled || !sql} - onClick={runSelect} - tooltip={{ - content: { - side: 'bottom', - className: 'max-w-56 text-center', - text: isExecuting - ? 'Query is running. Check the SQL Editor to manage running queries.' - : 'Run query', - }, - }} - /> + + } + loading={isExecuting} + disabled={isExecuting || disabled || !sql} + onClick={runSelect} + tooltip={{ + content: { + side: 'bottom', + className: 'max-w-56 text-center', + text: isExecuting + ? 'Query is running. Check the SQL Editor to manage running queries.' + : 'Run query', + }, + }} + /> + + )} - {actions} - - ) + {actions} + } > {!!showWarning && !blockWriteQueries && ( diff --git a/apps/studio/pages/reset-password.tsx b/apps/studio/pages/reset-password.tsx index d4aec3b39b931..8613000a38120 100644 --- a/apps/studio/pages/reset-password.tsx +++ b/apps/studio/pages/reset-password.tsx @@ -1,4 +1,4 @@ -import ResetPasswordForm from 'components/interfaces/SignIn/ResetPasswordForm' +import { ResetPasswordForm } from 'components/interfaces/SignIn/ResetPasswordForm' import ForgotPasswordLayout from 'components/layouts/SignInLayout/ForgotPasswordLayout' import { withAuth } from 'hooks/misc/withAuth' import type { NextPageWithLayout } from 'types' diff --git a/apps/studio/state/storage-explorer.tsx b/apps/studio/state/storage-explorer.tsx index fa1a9590e69e0..e4fcede7007a1 100644 --- a/apps/studio/state/storage-explorer.tsx +++ b/apps/studio/state/storage-explorer.tsx @@ -8,10 +8,6 @@ import * as tus from 'tus-js-client' import { Button, SONNER_DEFAULT_DURATION, SonnerProgress } from 'ui' import { proxy, useSnapshot } from 'valtio' -import { - inverseValidObjectKeyRegex, - validObjectKeyRegex, -} from '@/components/interfaces/Storage/CreateBucketModal.utils' import { STORAGE_BUCKET_SORT, STORAGE_ROW_STATUS, @@ -32,6 +28,10 @@ import { formatFolderItems, formatTime, getFilesDataTransferItems, + getPathAlongFoldersToIndex, + getPathAlongOpenedFolders, + sanitizeNameForDuplicateInColumn, + validateFolderName, } from '@/components/interfaces/Storage/StorageExplorer/StorageExplorer.utils' import { convertFromBytes } from '@/components/interfaces/Storage/StorageSettings/StorageSettings.utils' import { InlineLink } from '@/components/ui/InlineLink' @@ -73,7 +73,7 @@ const DEFAULT_PREFERENCES = { } const STORAGE_PROGRESS_INFO_TEXT = "Do not close the browser until it's completed" -let abortController: any +let abortController: AbortController if (typeof window !== 'undefined') { abortController = new AbortController() } @@ -100,7 +100,6 @@ function createStorageExplorerState({ resumableUploadUrl, uploadProgresses: [] as UploadProgress[], - // abortController, abortApiCalls: () => { if (abortController) { abortController.abort() @@ -133,7 +132,6 @@ function createStorageExplorerState({ popOpenedFolders: () => { state.openedFolders = state.openedFolders.slice(0, state.openedFolders.length - 1) }, - popOpenedFoldersAtIndex: (index: number) => { state.openedFolders = state.openedFolders.slice(0, index + 1) }, @@ -241,33 +239,6 @@ function createStorageExplorerState({ // ======== Folders CRUD ======== - getPathAlongOpenedFolders: (includeBucket = true) => { - if (includeBucket) { - return state.openedFolders.length > 0 - ? `${state.selectedBucket.name}/${state.openedFolders.map((folder) => folder.name).join('/')}` - : state.selectedBucket.name - } - return state.openedFolders.map((folder) => folder.name).join('/') - }, - - getPathAlongFoldersToIndex: (index: number) => { - return state.openedFolders - .slice(0, index) - .map((folder) => folder.name) - .join('/') - }, - - validateFolderName: (name: string) => { - if (!validObjectKeyRegex.test(name)) { - const [match] = name.match(inverseValidObjectKeyRegex) ?? [] - return !!match - ? `Folder name cannot contain the "${match}" character` - : 'Folder name contains an invalid special character' - } - - return null - }, - addNewFolderPlaceholder: (columnIndex: number) => { const isPrepend = true const folderName = 'Untitled folder' @@ -293,7 +264,7 @@ function createStorageExplorerState({ onError?: () => void }) => { const autofix = false - const formattedName = state.sanitizeNameForDuplicateInColumn({ + const formattedName = sanitizeNameForDuplicateInColumn(state, { name: folderName, autofix, columnIndex, @@ -308,7 +279,7 @@ function createStorageExplorerState({ return state.removeTempRows(columnIndex) } - const folderNameError = state.validateFolderName(formattedName) + const folderNameError = validateFolderName(formattedName) if (folderNameError) { onError?.() return toast.error(folderNameError) @@ -640,7 +611,7 @@ function createStorageExplorerState({ }) } - const folderNameError = state.validateFolderName(newName) + const folderNameError = validateFolderName(newName) if (folderNameError) { onError?.() return toast.error(folderNameError) @@ -1103,7 +1074,7 @@ function createStorageExplorerState({ const path = file.path.split('/') const topLevelFolder = path.length > 1 ? path[0] : null if (topLevelFolders.includes(topLevelFolder as string)) { - const newTopLevelFolder = state.sanitizeNameForDuplicateInColumn({ + const newTopLevelFolder = sanitizeNameForDuplicateInColumn(state, { name: topLevelFolder as string, autofix, columnIndex, @@ -1144,7 +1115,7 @@ function createStorageExplorerState({ const isWithinFolder = (file?.path ?? '').split('/').length > 1 const fileName = !isWithinFolder - ? state.sanitizeNameForDuplicateInColumn({ name: file.name, autofix }) + ? sanitizeNameForDuplicateInColumn(state, { name: file.name, autofix }) : file.name const unsanitizedFormattedFileName = has(file, ['path']) && isWithinFolder ? file.path : fileName @@ -1707,7 +1678,7 @@ function createStorageExplorerState({ columnIndex, updatedName: newName, }) - const pathToFile = state.getPathAlongFoldersToIndex(columnIndex) + const pathToFile = getPathAlongFoldersToIndex(state, columnIndex) const fromPath = pathToFile.length > 0 ? `${pathToFile}/${originalName}` : originalName const toPath = pathToFile.length > 0 ? `${pathToFile}/${newName}` : newName @@ -1777,54 +1748,6 @@ function createStorageExplorerState({ } }, - sanitizeNameForDuplicateInColumn: ({ - name, - columnIndex, - autofix = false, - }: { - name: string - columnIndex?: number - autofix?: boolean - }) => { - const columnIndex_ = columnIndex !== undefined ? columnIndex : state.getLatestColumnIndex() - const currentColumn = state.columns[columnIndex_] - const currentColumnItems = currentColumn.items.filter( - (item) => item.status !== STORAGE_ROW_STATUS.EDITING - ) - // [Joshen] JFYI storage does support folders of the same name with different casing - // but its an issue with the List V1 endpoint that's causing an issue with fetching contents - // for folders of the same name with different casing - // We should remove this check once all projects are on the List V2 endpoint - const hasSameNameInColumn = - currentColumnItems.filter((item) => item.name.toLowerCase() === name.toLowerCase()).length > - 0 - - if (hasSameNameInColumn) { - if (autofix) { - const fileNameSegments = name.split('.') - const fileName = fileNameSegments.slice(0, fileNameSegments.length - 1).join('.') - const fileExt = fileNameSegments[fileNameSegments.length - 1] - - const dupeNameRegex = new RegExp( - `${fileName} \\([-0-9]+\\)${fileExt ? '.' + fileExt : ''}$` - ) - const itemsWithSameNameInColumn = currentColumnItems.filter((item) => - item.name.match(dupeNameRegex) - ) - - const updatedFileName = fileName + ` (${itemsWithSameNameInColumn.length + 1})` - return fileExt ? `${updatedFileName}.${fileExt}` : updatedFileName - } else { - toast.error( - `The name ${name} already exists in the current directory. Please use a different name.` - ) - return null - } - } - - return name - }, - addTempRow: ({ type, name, @@ -1927,7 +1850,7 @@ function createStorageExplorerState({ return state } -type StorageExplorerState = ReturnType +export type StorageExplorerState = ReturnType const DEFAULT_STATE_CONFIG = { projectRef: '', diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts index 7286edec5fd23..4eb26581df46f 100644 --- a/packages/common/telemetry-constants.ts +++ b/packages/common/telemetry-constants.ts @@ -2320,7 +2320,7 @@ export interface SidebarOpenedEvent { /** * The sidebar panel that was opened, e.g. ai-assistant, editor-panel, advisor-panel */ - sidebar: 'ai-assistant' | 'editor-panel' | 'advisor-panel' + sidebar: 'ai-assistant' | 'editor-panel' | 'advisor-panel' | 'help-panel' } groups: TelemetryGroups } diff --git a/packages/ui-patterns/index.tsx b/packages/ui-patterns/index.tsx index 1be06287121d5..f7ffdde0b60fd 100644 --- a/packages/ui-patterns/index.tsx +++ b/packages/ui-patterns/index.tsx @@ -16,6 +16,7 @@ export * from './src/FilterBar' export * from './src/GlassPanel' export * from './src/InnerSideMenu' export * from './src/McpUrlBuilder' +export * from './src/MobileSheetNav' export * from './src/PageContainer' export * from './src/PageHeader' export * from './src/PageSection' diff --git a/packages/ui-patterns/src/MobileSheetNav/MobileSheetNav.test.tsx b/packages/ui-patterns/src/MobileSheetNav/MobileSheetNav.test.tsx new file mode 100644 index 0000000000000..65137313efda0 --- /dev/null +++ b/packages/ui-patterns/src/MobileSheetNav/MobileSheetNav.test.tsx @@ -0,0 +1,199 @@ +import { act, render, screen, waitFor } from '@testing-library/react' +import { useEffect, useState } from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { MobileSheetNav } from './MobileSheetNav' + +const mockRouter = vi.hoisted(() => ({ asPath: '/initial' })) +const mockWindowSize = vi.hoisted(() => ({ width: 400 })) + +vi.mock('next/router', () => ({ + useRouter: () => mockRouter, +})) + +vi.mock('react-use', () => ({ + useWindowSize: () => mockWindowSize, +})) + +function MobileSheetNavWithState({ + shouldCloseOnRouteChange = true, + shouldCloseOnViewportResize = true, +}: { + shouldCloseOnRouteChange?: boolean + shouldCloseOnViewportResize?: boolean +}) { + const [open, setOpen] = useState(false) + useEffect(() => { + setOpen(true) + }, []) + return ( + <> + {String(open)} + +
Nav content
+
+ + ) +} + +describe('MobileSheetNav', () => { + const defaultProps = { + onOpenChange: vi.fn(), + children:
Nav content
, + } + + beforeEach(() => { + vi.clearAllMocks() + mockRouter.asPath = '/initial' + mockWindowSize.width = 400 + }) + + describe('shouldCloseOnRouteChange', () => { + it('calls onOpenChange(false) when route changes and shouldCloseOnRouteChange is true (default)', () => { + const onOpenChange = vi.fn() + const { rerender } = render() + + onOpenChange.mockClear() + mockRouter.asPath = '/other-page' + act(() => { + rerender() + }) + + expect(onOpenChange).toHaveBeenCalledWith(false) + }) + + it('effectively closes the sheet on route change when shouldCloseOnRouteChange is true', async () => { + const { rerender } = render() + + await waitFor(() => { + expect(screen.getByTestId('sheet-open')).toHaveTextContent('true') + }) + + mockRouter.asPath = '/other-page' + act(() => { + rerender() + }) + + await waitFor(() => { + expect(screen.getByTestId('sheet-open')).toHaveTextContent('false') + }) + }) + + it('effectively does NOT close the sheet on route change when shouldCloseOnRouteChange is false', async () => { + const { rerender } = render() + + await waitFor(() => { + expect(screen.getByTestId('sheet-open')).toHaveTextContent('true') + }) + + mockRouter.asPath = '/other-page' + act(() => { + rerender() + }) + + expect(screen.getByTestId('sheet-open')).toHaveTextContent('true') + }) + + it('does not call onOpenChange when route changes if shouldCloseOnRouteChange is false', () => { + const onOpenChange = vi.fn() + const { rerender } = render( + + ) + + onOpenChange.mockClear() + mockRouter.asPath = '/other-page' + act(() => { + rerender( + + ) + }) + + expect(onOpenChange).not.toHaveBeenCalled() + }) + }) + + describe('shouldCloseOnViewportResize', () => { + it('calls onOpenChange(false) when width changes and shouldCloseOnViewportResize is true (default)', () => { + const onOpenChange = vi.fn() + const { rerender } = render() + + onOpenChange.mockClear() + mockWindowSize.width = 800 + act(() => { + rerender() + }) + + expect(onOpenChange).toHaveBeenCalledWith(false) + }) + + it('effectively closes the sheet on viewport resize when shouldCloseOnViewportResize is true', async () => { + const { rerender } = render() + + await waitFor(() => { + expect(screen.getByTestId('sheet-open')).toHaveTextContent('true') + }) + + mockWindowSize.width = 800 + act(() => { + rerender() + }) + + await waitFor(() => { + expect(screen.getByTestId('sheet-open')).toHaveTextContent('false') + }) + }) + + it('effectively does NOT close the sheet on viewport resize when shouldCloseOnViewportResize is false', async () => { + const { rerender } = render() + + await waitFor(() => { + expect(screen.getByTestId('sheet-open')).toHaveTextContent('true') + }) + + mockWindowSize.width = 800 + act(() => { + rerender() + }) + + expect(screen.getByTestId('sheet-open')).toHaveTextContent('true') + }) + + it('does not call onOpenChange when width changes if shouldCloseOnViewportResize is false', () => { + const onOpenChange = vi.fn() + const { rerender } = render( + + ) + + onOpenChange.mockClear() + mockWindowSize.width = 800 + act(() => { + rerender( + + ) + }) + + expect(onOpenChange).not.toHaveBeenCalled() + }) + }) +}) diff --git a/packages/ui-patterns/src/MobileSheetNav/MobileSheetNav.tsx b/packages/ui-patterns/src/MobileSheetNav/MobileSheetNav.tsx index 6e06fc0621dd1..064035ac60739 100644 --- a/packages/ui-patterns/src/MobileSheetNav/MobileSheetNav.tsx +++ b/packages/ui-patterns/src/MobileSheetNav/MobileSheetNav.tsx @@ -11,17 +11,31 @@ const MobileSheetNav: React.FC<{ children: React.ReactNode open?: boolean onOpenChange(open: boolean): void -}> = ({ children, open = false, onOpenChange }) => { + className?: string + shouldCloseOnRouteChange?: boolean + shouldCloseOnViewportResize?: boolean +}> = ({ + children, + open = false, + onOpenChange, + className, + shouldCloseOnRouteChange = true, + shouldCloseOnViewportResize = true, +}) => { const router = useRouter() const { width } = useWindowSize() const pathWithoutQuery = router?.asPath?.split('?')?.[0] useEffect(() => { - onOpenChange(false) + if (shouldCloseOnRouteChange) { + onOpenChange(false) + } }, [pathWithoutQuery]) useEffect(() => { - onOpenChange(false) + if (shouldCloseOnViewportResize) { + onOpenChange(false) + } }, [width]) return ( @@ -31,7 +45,10 @@ const MobileSheetNav: React.FC<{ showClose={false} size="full" side="bottom" - className={cn('rounded-t-lg overflow-hidden overflow-y-scroll h-[85dvh] md:max-h-[500px]')} + className={cn( + 'rounded-t-lg bg-background overflow-hidden overflow-y-scroll h-[85dvh] md:max-h-[500px]', + className + )} > }>{children} @@ -39,4 +56,5 @@ const MobileSheetNav: React.FC<{ ) } +export { MobileSheetNav } export default MobileSheetNav diff --git a/packages/ui-patterns/src/MobileSheetNav/index.ts b/packages/ui-patterns/src/MobileSheetNav/index.ts index fd58868d48141..06b0a8256915b 100644 --- a/packages/ui-patterns/src/MobileSheetNav/index.ts +++ b/packages/ui-patterns/src/MobileSheetNav/index.ts @@ -1 +1 @@ -export * from './MobileSheetNav' +export { default as MobileSheetNav } from './MobileSheetNav' \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1fdf57c8ef973..8fc7135cb2ae4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,17 +10,17 @@ catalogs: specifier: ^10.26.0 version: 10.27.0 '@supabase/auth-js': - specifier: 2.97.1-canary.3 - version: 2.97.1-canary.3 + specifier: 2.98.0 + version: 2.98.0 '@supabase/postgrest-js': - specifier: 2.97.1-canary.3 - version: 2.97.1-canary.3 + specifier: 2.98.0 + version: 2.98.0 '@supabase/realtime-js': - specifier: 2.97.1-canary.3 - version: 2.97.1-canary.3 + specifier: 2.98.0 + version: 2.98.0 '@supabase/supabase-js': - specifier: 2.97.1-canary.3 - version: 2.97.1-canary.3 + specifier: 2.98.0 + version: 2.98.0 '@types/node': specifier: ^22.0.0 version: 22.13.14 @@ -317,7 +317,7 @@ importers: version: 10.27.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.10(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.97.1-canary.3 + version: 2.98.0 '@tailwindcss/container-queries': specifier: ^0.1.1 version: 0.1.1(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2))) @@ -829,7 +829,7 @@ importers: version: 7.5.0 '@supabase/auth-js': specifier: 'catalog:' - version: 2.97.1-canary.3 + version: 2.98.0 '@supabase/mcp-server-supabase': specifier: ^0.6.3 version: 0.6.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(supports-color@8.1.1)(zod@3.25.76))(zod@3.25.76) @@ -841,7 +841,7 @@ importers: version: link:../../packages/pg-meta '@supabase/realtime-js': specifier: 'catalog:' - version: 2.97.1-canary.3 + version: 2.98.0 '@supabase/shared-types': specifier: 0.1.84 version: 0.1.84 @@ -850,7 +850,7 @@ importers: version: 0.1.6(encoding@0.1.13)(supports-color@8.1.1) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.97.1-canary.3 + version: 2.98.0 '@tanstack/react-query': specifier: ^5.0.0 version: 5.83.0(react@18.3.1) @@ -1367,7 +1367,7 @@ importers: version: 7.4.0(@react-router/dev@7.9.6(@types/node@22.13.14)(@vitejs/plugin-rsc@0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(jiti@2.5.1)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.2) '@supabase/postgrest-js': specifier: 'catalog:' - version: 2.97.1-canary.3 + version: 2.98.0 '@supabase/supa-mdx-lint': specifier: 0.2.6-alpha version: 0.2.6-alpha @@ -1488,10 +1488,10 @@ importers: version: 1.6.0 '@supabase/ssr': specifier: ^0.7.0 - version: 0.7.0(@supabase/supabase-js@2.97.1-canary.3) + version: 0.7.0(@supabase/supabase-js@2.98.0) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.97.1-canary.3 + version: 2.98.0 '@tanstack/react-router': specifier: ^1.150.0 version: 1.158.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1602,7 +1602,7 @@ importers: version: 10.27.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.5.10(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.94.0) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.97.1-canary.3 + version: 2.98.0 '@vercel/og': specifier: ^0.6.2 version: 0.6.2 @@ -1849,10 +1849,10 @@ importers: dependencies: '@supabase/ssr': specifier: ^0.7.0 - version: 0.7.0(@supabase/supabase-js@2.97.1-canary.3) + version: 0.7.0(@supabase/supabase-js@2.98.0) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.97.1-canary.3 + version: 2.98.0 '@vueuse/core': specifier: ^14.1.0 version: 14.1.0(vue@3.5.21(typescript@5.9.2)) @@ -1898,7 +1898,7 @@ importers: version: 1.56.1 '@supabase/supabase-js': specifier: 'catalog:' - version: 2.97.1-canary.3 + version: 2.98.0 cross-fetch: specifier: ^4.1.0 version: 4.1.0(encoding@0.1.13) @@ -1926,7 +1926,7 @@ importers: version: 0.18.5 '@supabase/supabase-js': specifier: 'catalog:' - version: 2.97.1-canary.3 + version: 2.98.0 ai: specifier: 5.0.52 version: 5.0.52(zod@3.25.76) @@ -2020,10 +2020,10 @@ importers: dependencies: '@supabase/auth-js': specifier: 'catalog:' - version: 2.97.1-canary.3 + version: 2.98.0 '@supabase/supabase-js': specifier: 'catalog:' - version: 2.97.1-canary.3 + version: 2.98.0 '@types/dat.gui': specifier: ^0.7.12 version: 0.7.12 @@ -2582,7 +2582,7 @@ importers: version: 0.1.6(encoding@0.1.13)(supports-color@8.1.1) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.97.1-canary.3 + version: 2.98.0 '@vitest/coverage-v8': specifier: ^3.2.0 version: 3.2.4(supports-color@8.1.1)(vitest@3.2.4) @@ -8209,12 +8209,12 @@ packages: resolution: {integrity: sha512-Cq3KKe+G1o7PSBMbmrgpT2JgBeyH2THHr3RdIX2MqF7AnBuspIMgtZ3ktcCgP7kZsTMvnmWymr7zZCT1zeWbMw==} engines: {node: '>=12.16'} - '@supabase/auth-js@2.97.1-canary.3': - resolution: {integrity: sha512-MLsim92wAP6zC20dfl98HlZmpNdVWCQHvuaVvorT1wXiqe4n6iseGtLXhHrC1rBsV8UreEJ7TltPa97bUAH32w==} + '@supabase/auth-js@2.98.0': + resolution: {integrity: sha512-GBH361T0peHU91AQNzOlIrjUZw9TZbB9YDRiyFgk/3Kvr3/Z1NWUZ2athWTfHhwNNi8IrW00foyFxQD9IO/Trg==} engines: {node: '>=20.0.0'} - '@supabase/functions-js@2.97.1-canary.3': - resolution: {integrity: sha512-lgjHUVnRwsmtlbrfBvq/BTK8SIzzX8zGskjLQpxI0SRVeppJ0/krcrhgqys4bdns5bGOfn2QIRqhGSW4eDpj5g==} + '@supabase/functions-js@2.98.0': + resolution: {integrity: sha512-N/xEyiNU5Org+d+PNCpv+TWniAXRzxIURxDYsS/m2I/sfAB/HcM9aM2Dmf5edj5oWb9GxID1OBaZ8NMmPXL+Lg==} engines: {node: '>=20.0.0'} '@supabase/mcp-server-supabase@0.6.3': @@ -8234,12 +8234,12 @@ packages: resolution: {integrity: sha512-vz5gc6RKNfDVnIfRUmH2ssTMYFI0U3MYOVyQ9R4YkzOS2dKSanjC4rTEDGjlMFwGTCUPW3N3pbY7HJIW81wMyg==} engines: {node: '>=16', npm: '>=8'} - '@supabase/postgrest-js@2.97.1-canary.3': - resolution: {integrity: sha512-M8IVst0FYK7LunTyVDorT2PVn9R9p1u0dBAsqOUxhZDdhvUiMrU2hq8xlUK2F6TEMN2YHLjCC3tFm9rfg9R54g==} + '@supabase/postgrest-js@2.98.0': + resolution: {integrity: sha512-v6e9WeZuJijzUut8HyXu6gMqWFepIbaeaMIm1uKzei4yLg9bC9OtEW9O14LE/9ezqNbSAnSLO5GtOLFdm7Bpkg==} engines: {node: '>=20.0.0'} - '@supabase/realtime-js@2.97.1-canary.3': - resolution: {integrity: sha512-jZ+UK2M7aQbXze5p+IAt02jLZdDDCP981WdrhoUnQR536WdY385tRqAMoKgl21xCpozAo3maO4VraOhagipUtg==} + '@supabase/realtime-js@2.98.0': + resolution: {integrity: sha512-rOWt28uGyFipWOSd+n0WVMr9kUXiWaa7J4hvyLCIHjRFqWm1z9CaaKAoYyfYMC1Exn3WT8WePCgiVhlAtWC2yw==} engines: {node: '>=20.0.0'} '@supabase/shared-types@0.1.84': @@ -8253,8 +8253,8 @@ packages: peerDependencies: '@supabase/supabase-js': ^2.43.4 - '@supabase/storage-js@2.97.1-canary.3': - resolution: {integrity: sha512-2QK8NR8OFtiLmyHWduG/6Gi+6ArJvmEzD5yegLeqbsh0VAoBoW4qUCQyiCNYbua7RPpb72zsaaKQg4wO2y6ZIw==} + '@supabase/storage-js@2.98.0': + resolution: {integrity: sha512-tzr2mG+v7ILSAZSfZMSL9OPyIH4z1ikgQ8EcQTKfMRz4EwmlFt3UnJaGzSOxyvF5b+fc9So7qdSUWTqGgeLokQ==} engines: {node: '>=20.0.0'} '@supabase/supa-mdx-lint-darwin@0.2.6-alpha': @@ -8392,8 +8392,8 @@ packages: resolution: {integrity: sha512-iqJHDk/ToyxFMa/um9A5gsp9jYN7iI0cIabNq9nyBpK/Yat/J9I2IIMueQwIMy3TsG6a2qdPyUkZkuPC37SdRg==} hasBin: true - '@supabase/supabase-js@2.97.1-canary.3': - resolution: {integrity: sha512-fpZT3obVxsNvjdBEYeGqcGDaF9XsNYuGN9bKFt4Ooqwhe97y9tpf72sswqiRzuFRmgxmZ1dFnAMiFveTeoQDwQ==} + '@supabase/supabase-js@2.98.0': + resolution: {integrity: sha512-Ohc97CtInLwZyiSASz7tT9/Abm/vqnIbO9REp+PivVUII8UZsuI3bngRQnYgJdFoOIwvaEII1fX1qy8x0CyNiw==} engines: {node: '>=20.0.0'} '@swc/helpers@0.5.15': @@ -25542,11 +25542,11 @@ snapshots: '@stripe/stripe-js@7.5.0': {} - '@supabase/auth-js@2.97.1-canary.3': + '@supabase/auth-js@2.98.0': dependencies: tslib: 2.8.1 - '@supabase/functions-js@2.97.1-canary.3': + '@supabase/functions-js@2.98.0': dependencies: tslib: 2.8.1 @@ -25581,11 +25581,11 @@ snapshots: - pg-native - supports-color - '@supabase/postgrest-js@2.97.1-canary.3': + '@supabase/postgrest-js@2.98.0': dependencies: tslib: 2.8.1 - '@supabase/realtime-js@2.97.1-canary.3': + '@supabase/realtime-js@2.98.0': dependencies: '@types/phoenix': 1.6.6 '@types/ws': 8.18.1 @@ -25606,12 +25606,12 @@ snapshots: - encoding - supports-color - '@supabase/ssr@0.7.0(@supabase/supabase-js@2.97.1-canary.3)': + '@supabase/ssr@0.7.0(@supabase/supabase-js@2.98.0)': dependencies: - '@supabase/supabase-js': 2.97.1-canary.3 + '@supabase/supabase-js': 2.98.0 cookie: 1.0.2 - '@supabase/storage-js@2.97.1-canary.3': + '@supabase/storage-js@2.98.0': dependencies: iceberg-js: 0.8.1 tslib: 2.8.1 @@ -25712,13 +25712,13 @@ snapshots: '@supabase/supa-mdx-lint-win32-x64': 0.3.2 node-pty: 1.0.0 - '@supabase/supabase-js@2.97.1-canary.3': + '@supabase/supabase-js@2.98.0': dependencies: - '@supabase/auth-js': 2.97.1-canary.3 - '@supabase/functions-js': 2.97.1-canary.3 - '@supabase/postgrest-js': 2.97.1-canary.3 - '@supabase/realtime-js': 2.97.1-canary.3 - '@supabase/storage-js': 2.97.1-canary.3 + '@supabase/auth-js': 2.98.0 + '@supabase/functions-js': 2.98.0 + '@supabase/postgrest-js': 2.98.0 + '@supabase/realtime-js': 2.98.0 + '@supabase/storage-js': 2.98.0 transitivePeerDependencies: - bufferutil - utf-8-validate diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 92e2730fc4334..1f0deece909f6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,10 +6,10 @@ packages: catalog: '@sentry/nextjs': ^10.26.0 - '@supabase/auth-js': 2.97.1-canary.3 - '@supabase/postgrest-js': 2.97.1-canary.3 - '@supabase/realtime-js': 2.97.1-canary.3 - '@supabase/supabase-js': 2.97.1-canary.3 + '@supabase/auth-js': 2.98.0 + '@supabase/postgrest-js': 2.98.0 + '@supabase/realtime-js': 2.98.0 + '@supabase/supabase-js': 2.98.0 '@types/node': ^22.0.0 '@types/react': ^18.3.0 '@types/react-dom': ^18.3.0