From 06c2039d0bbf012a5e1614eb50a3dd49eaf3463e Mon Sep 17 00:00:00 2001 From: Jeremias Menichelli Date: Fri, 17 Jul 2026 14:58:35 +0200 Subject: [PATCH 1/7] test: Simplify setup (#48036) --- apps/docs/middleware.test.ts | 8 +++++++- apps/docs/package.json | 5 +---- apps/docs/types/manifest.d.ts | 6 ++++++ 3 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 apps/docs/types/manifest.d.ts diff --git a/apps/docs/middleware.test.ts b/apps/docs/middleware.test.ts index 2c0c111e939fb..87d171621d9ac 100644 --- a/apps/docs/middleware.test.ts +++ b/apps/docs/middleware.test.ts @@ -1,8 +1,14 @@ import { NextRequest } from 'next/server' -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { middleware } from './middleware' +// The real manifest is generated at build time by a content script; mocking +// it here decouples this test from that build step. +vi.mock('~/public/markdown/manifest.json', () => ({ + default: ['auth'], +})) + // BASE_PATH defaults to '/docs' when NEXT_PUBLIC_BASE_PATH is unset, so test // paths include the /docs prefix to match the middleware's GUIDES_PATH check. function makeRequest( diff --git a/apps/docs/package.json b/apps/docs/package.json index 737d46427f83a..704b3a23cccff 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -42,10 +42,7 @@ "preinstall": "npx only-allow pnpm", "presync": "pnpm run codegen:graphql", "pretest": "pnpm run codegen:examples", - "pretest:local": "pnpm run build:guides-markdown", - "pretest:local:unwatch": "pnpm run build:guides-markdown", - "pretest:smoke": "pnpm run build:guides-markdown", - "pretypecheck": "pnpm run codegen:graphql && pnpm run build:markdown && next typegen", + "pretypecheck": "pnpm run codegen:graphql && next typegen", "start": "next start", "sync": "tsx --conditions=react-server ./resources/rootSync.ts", "test": "pnpm supabase start && pnpm run test:local && pnpm supabase stop", diff --git a/apps/docs/types/manifest.d.ts b/apps/docs/types/manifest.d.ts new file mode 100644 index 0000000000000..191ccfe543693 --- /dev/null +++ b/apps/docs/types/manifest.d.ts @@ -0,0 +1,6 @@ +// public/markdown/manifest.json is generated by internals/generate-guides-markdown.ts +// and gitignored, so it may not exist on disk when tsc runs. +declare module '~/public/markdown/manifest.json' { + const slugs: string[] + export default slugs +} From dc3c8684cc4b68a84145b82c058521ed1eb3fb84 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Fri, 17 Jul 2026 15:51:54 +0200 Subject: [PATCH 2/7] chore(deps): upgrade valtio to v2 (#48031) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audited all proxy()/useSnapshot() usage against the v1→v2 migration guide; no breaking changes apply (no reused proxy() inputs, no promise-valued state, all consumers already client components). ## Summary by CodeRabbit * **Chores** * Updated the Valtio dependency to a newer version for improved compatibility. * **Bug Fixes** * Improved AI assistant persistence in IndexedDB so chat sessions reliably save (while keeping only the most recent 20 messages per chat). * Hardened tabs restoration from storage to fall back to fresh defaults when data is missing, invalid, or fails validation. * **Refactor** * Switched multiple studio panels to use fresh initial-state factories for initialization and reset reliability. * Updated advisor state so the derived notification filter count is no longer exposed. --- .../BucketFilePickerState.tsx | 6 +-- apps/studio/state/advisor-state.ts | 11 ++--- apps/studio/state/ai-assistant-state.tsx | 44 +++++++++-------- apps/studio/state/editor-panel-state.tsx | 8 ++-- apps/studio/state/sidebar-manager-state.tsx | 6 +-- apps/studio/state/storage-explorer.tsx | 8 ++-- apps/studio/state/tabs.tsx | 18 +++---- pnpm-lock.yaml | 47 +++++++------------ pnpm-workspace.yaml | 2 +- 9 files changed, 70 insertions(+), 80 deletions(-) diff --git a/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerState.tsx b/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerState.tsx index 7ff3c850a1d37..c62f89ab2589f 100644 --- a/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerState.tsx +++ b/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerState.tsx @@ -52,13 +52,13 @@ function createBucketFilePickerState({ export type BucketFilePickerState = ReturnType -const DEFAULT_STATE_CONFIG = { +const createDefaultStateConfig = () => ({ bucket: {} as Bucket, maxFiles: 1 as const, -} +}) const BucketFilePickerStateContext = createContext( - createBucketFilePickerState(DEFAULT_STATE_CONFIG) + createBucketFilePickerState(createDefaultStateConfig()) ) export const BucketFilePickerStateContextProvider = ({ diff --git a/apps/studio/state/advisor-state.ts b/apps/studio/state/advisor-state.ts index 8bf49ba6a0b6c..adb66003ef78e 100644 --- a/apps/studio/state/advisor-state.ts +++ b/apps/studio/state/advisor-state.ts @@ -4,7 +4,7 @@ export type AdvisorTab = 'all' | 'security' | 'performance' | 'messages' export type AdvisorSeverity = 'critical' | 'warning' | 'info' export type AdvisorItemSource = 'lint' | 'notification' | 'signal' -const initialState = { +const createInitialState = () => ({ activeTab: 'all' as AdvisorTab, severityFilters: ['critical', 'warning'] as AdvisorSeverity[], selectedItemId: undefined as string | undefined, @@ -12,13 +12,10 @@ const initialState = { // Notification filters notificationFilterStatuses: [] as string[], notificationFilterPriorities: [] as string[], - get numNotificationFiltersApplied() { - return [...this.notificationFilterStatuses, ...this.notificationFilterPriorities].length - }, -} +}) export const advisorState = proxy({ - ...initialState, + ...createInitialState(), setActiveTab(tab: AdvisorTab) { advisorState.activeTab = tab }, @@ -68,7 +65,7 @@ export const advisorState = proxy({ advisorState.notificationFilterPriorities = [] }, reset() { - Object.assign(advisorState, initialState) + Object.assign(advisorState, createInitialState()) }, }) diff --git a/apps/studio/state/ai-assistant-state.tsx b/apps/studio/state/ai-assistant-state.tsx index f7e59494d948b..c083fa10e52c7 100644 --- a/apps/studio/state/ai-assistant-state.tsx +++ b/apps/studio/state/ai-assistant-state.tsx @@ -88,7 +88,7 @@ type StoredAiAssistantState = { model?: AssistantModel } -const INITIAL_AI_ASSISTANT: AiAssistantData = { +const createInitialAiAssistantData = (): AiAssistantData => ({ initialInput: '', sqlSnippets: undefined, suggestions: undefined, @@ -97,7 +97,7 @@ const INITIAL_AI_ASSISTANT: AiAssistantData = { activeChatId: undefined, model: undefined, context: {}, -} +}) const DB_NAME = 'ai-assistant-db' const DB_VERSION = 1 @@ -210,7 +210,7 @@ async function tryMigrateFromLocalStorage( projectRef: projectRef, activeChatId: parsedFromLocalStorage.activeChatId, chats: parsedFromLocalStorage.chats, - model: parsedFromLocalStorage.model ?? INITIAL_AI_ASSISTANT.model, + model: parsedFromLocalStorage.model ?? createInitialAiAssistantData().model, } } else { console.warn('Data in localStorage is not in the expected format, ignoring.') @@ -356,7 +356,7 @@ function createChatInstance( export const createAiAssistantState = (): AiAssistantState => { // Initialize with defaults, loading happens asynchronously in the provider - const initialState = { ...INITIAL_AI_ASSISTANT } + const initialState = createInitialAiAssistantData() const state: AiAssistantState = proxy({ ...initialState, // Spread initial values directly @@ -369,7 +369,7 @@ export const createAiAssistantState = (): AiAssistantState => { }, resetAiAssistantPanel: () => { - Object.assign(state, INITIAL_AI_ASSISTANT) + Object.assign(state, createInitialAiAssistantData()) }, setModel: (model: AssistantModel) => { @@ -414,10 +414,11 @@ export const createAiAssistantState = (): AiAssistantState => { } // Update non-chat related state based on options, falling back to current state, then initial - state.initialInput = options?.initialInput ?? INITIAL_AI_ASSISTANT.initialInput - state.sqlSnippets = options?.sqlSnippets ?? INITIAL_AI_ASSISTANT.sqlSnippets - state.suggestions = options?.suggestions ?? INITIAL_AI_ASSISTANT.suggestions - state.tables = options?.tables ?? INITIAL_AI_ASSISTANT.tables + const initialAiAssistantData = createInitialAiAssistantData() + state.initialInput = options?.initialInput ?? initialAiAssistantData.initialInput + state.sqlSnippets = options?.sqlSnippets ?? initialAiAssistantData.sqlSnippets + state.suggestions = options?.suggestions ?? initialAiAssistantData.suggestions + state.tables = options?.tables ?? initialAiAssistantData.tables return chatId }, @@ -561,7 +562,7 @@ export const createAiAssistantState = (): AiAssistantState => { state.model = storedModel && isKnownAssistantModelId(storedModel) ? storedModel - : INITIAL_AI_ASSISTANT.model + : createInitialAiAssistantData().model // Reset sync guards on any support chats (can't be mid-sync after reload) Object.values(state.chats).forEach((chat) => { @@ -698,22 +699,25 @@ export const AiAssistantStateContextProvider = ({ children }: PropsWithChildren) const unsubscribe = subscribe(state, () => { const snap = snapshot(state) + // Prepare state for IndexedDB const stateToSave: StoredAiAssistantState = { projectRef: project?.ref, activeChatId: snap.activeChatId, model: snap.model, chats: snap.chats - ? Object.entries(snap.chats).reduce((acc, [chatId, chat]) => { - // Limit messages before saving - return { - ...acc, - [chatId]: { - ...chat, - messages: chat.messages?.slice(-20) || [], - }, - } - }, {}) + ? (Object.entries(snap.chats) as Array<[string, ChatSession]>).reduce( + (acc, [chatId, chat]) => { + return { + ...acc, + [chatId]: { + ...chat, + messages: chat.messages?.slice(-20) || [], + }, + } + }, + {} as Record + ) : {}, } debouncedSaveAiState(stateToSave) diff --git a/apps/studio/state/editor-panel-state.tsx b/apps/studio/state/editor-panel-state.tsx index 79b41e586b516..723066a5d6ec9 100644 --- a/apps/studio/state/editor-panel-state.tsx +++ b/apps/studio/state/editor-panel-state.tsx @@ -24,7 +24,7 @@ type EditorPanelState = { pendingReset: boolean } -const initialState: EditorPanelState = { +const createInitialState = (): EditorPanelState => ({ value: safeSql``, templates: [], results: undefined, @@ -33,10 +33,10 @@ const initialState: EditorPanelState = { onChange: undefined, activeSnippetId: null, pendingReset: false, -} +}) export const editorPanelState = proxy({ - ...initialState, + ...createInitialState(), setValue(value: DisplayableSqlFragment) { editorPanelState.value = value editorPanelState.onChange?.(value) @@ -65,7 +65,7 @@ export const editorPanelState = proxy({ editorPanelState.pendingReset = true }, reset() { - Object.assign(editorPanelState, initialState) + Object.assign(editorPanelState, createInitialState()) }, }) diff --git a/apps/studio/state/sidebar-manager-state.tsx b/apps/studio/state/sidebar-manager-state.tsx index 1a3f1f4ac59e7..77a978b8f1513 100644 --- a/apps/studio/state/sidebar-manager-state.tsx +++ b/apps/studio/state/sidebar-manager-state.tsx @@ -36,16 +36,16 @@ type SidebarManagerState = SidebarManagerData & { toggleMaximise: () => void } -const INITIAL_SIDEBAR_MANAGER_DATA: SidebarManagerData = { +const createInitialSidebarManagerData = (): SidebarManagerData => ({ sidebars: {}, activeSidebar: undefined, pendingSidebarOpen: undefined, isMaximised: false, -} +}) const createSidebarManagerState = () => { const state: SidebarManagerState = proxy({ - ...INITIAL_SIDEBAR_MANAGER_DATA, + ...createInitialSidebarManagerData(), registerSidebar( id: string, diff --git a/apps/studio/state/storage-explorer.tsx b/apps/studio/state/storage-explorer.tsx index 1bb58b10c3ff4..d4fd8a846910e 100644 --- a/apps/studio/state/storage-explorer.tsx +++ b/apps/studio/state/storage-explorer.tsx @@ -1839,16 +1839,16 @@ function createStorageExplorerState({ export type StorageExplorerState = ReturnType -const DEFAULT_STATE_CONFIG = { +const createDefaultStateConfig = () => ({ projectRef: '', connectionString: '', resumableUploadUrl: '', clientEndpoint: '', bucket: {} as Bucket, -} +}) const StorageExplorerStateContext = createContext( - createStorageExplorerState(DEFAULT_STATE_CONFIG) + createStorageExplorerState(createDefaultStateConfig()) ) export const StorageExplorerStateContextProvider = ({ children }: PropsWithChildren) => { @@ -1856,7 +1856,7 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild const { data: bucket } = useSelectedBucket() const isPaused = project?.status === PROJECT_STATUS.INACTIVE - const [state, setState] = useState(() => createStorageExplorerState(DEFAULT_STATE_CONFIG)) + const [state, setState] = useState(() => createStorageExplorerState(createDefaultStateConfig())) const stateRef = useLatest(state) const { diff --git a/apps/studio/state/tabs.tsx b/apps/studio/state/tabs.tsx index 2b72256579bb5..c0ef9b95fe2d0 100644 --- a/apps/studio/state/tabs.tsx +++ b/apps/studio/state/tabs.tsx @@ -116,27 +116,27 @@ function getSavedRecentItems(ref: string): RecentItem[] { } } -const DEFAULT_TABS_STATE = { +const createDefaultTabsState = () => ({ activeTab: null as string | null, openTabs: [] as string[], tabsMap: {} as Record, previewTabId: undefined as string | undefined, recentItems: [], -} +}) const TABS_STORAGE_KEY = 'supabase_studio_tabs' const getTabsStorageKey = (ref: string) => `${TABS_STORAGE_KEY}_${ref}` function getSavedTabs(ref: string) { - if (!ref) return DEFAULT_TABS_STATE + if (!ref) return createDefaultTabsState() const stored = safeLocalStorage.getItem(getTabsStorageKey(ref)) - if (!stored) return DEFAULT_TABS_STATE + if (!stored) return createDefaultTabsState() try { - const parsed = JSON.parse( - stored ?? JSON.stringify(DEFAULT_TABS_STATE) - ) as typeof DEFAULT_TABS_STATE + const parsed = JSON.parse(stored ?? JSON.stringify(createDefaultTabsState())) as ReturnType< + typeof createDefaultTabsState + > if ( !parsed.openTabs || @@ -144,12 +144,12 @@ function getSavedTabs(ref: string) { !parsed.tabsMap || typeof parsed.tabsMap !== 'object' ) { - return DEFAULT_TABS_STATE + return createDefaultTabsState() } return parsed } catch (error) { - return DEFAULT_TABS_STATE + return createDefaultTabsState() } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d816ed3c30eb0..28c11dee30876 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,8 +88,8 @@ catalogs: specifier: ~6.0.2 version: 6.0.2 valtio: - specifier: ^1.12.0 - version: 1.12.0 + specifier: ^2.3.2 + version: 2.3.2 vite: specifier: ^8.0.16 version: 8.0.16 @@ -581,7 +581,7 @@ importers: version: 14.0.0 valtio: specifier: 'catalog:' - version: 1.12.0(@types/react@19.2.14)(react@19.2.6) + version: 2.3.2(@types/react@19.2.14)(react@19.2.6) yaml: specifier: ^2.8.1 version: 2.9.0 @@ -1229,7 +1229,7 @@ importers: version: 14.0.0 valtio: specifier: 'catalog:' - version: 1.12.0(@types/react@19.2.14)(react@19.2.6) + version: 2.3.2(@types/react@19.2.14)(react@19.2.6) zod: specifier: 'catalog:' version: 3.25.76 @@ -2180,7 +2180,7 @@ importers: version: 17.6.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) valtio: specifier: 'catalog:' - version: 1.12.0(@types/react@19.2.14)(react@19.2.6) + version: 2.3.2(@types/react@19.2.14)(react@19.2.6) devDependencies: '@types/lodash': specifier: 4.17.5 @@ -2742,7 +2742,7 @@ importers: version: 5.0.0 valtio: specifier: 'catalog:' - version: 1.12.0(@types/react@19.2.14)(react@19.2.6) + version: 2.3.2(@types/react@19.2.14)(react@19.2.6) zod: specifier: 'catalog:' version: 3.25.76 @@ -10605,11 +10605,6 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} - derive-valtio@0.1.0: - resolution: {integrity: sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==} - peerDependencies: - valtio: '*' - destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} @@ -14988,8 +14983,8 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} - proxy-compare@2.5.1: - resolution: {integrity: sha512-oyfc0Tx87Cpwva5ZXezSp5V9vht1c7dZBhvuV/y3ctkgMVUmiAGDVeeB0dKhGSyT0v1ZTEQYpe/RXlBVBNuCLA==} + proxy-compare@3.0.1: + resolution: {integrity: sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==} proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -17042,12 +17037,12 @@ packages: validate.io-function@1.0.2: resolution: {integrity: sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ==} - valtio@1.12.0: - resolution: {integrity: sha512-co8NkCHeY0NsL0XsL/cSICt5VhTjwZlYT8mi50dYY5thx3r3w1D15A04Lvs9WL/y/Rf98vUKY5PAAJCTLHvkJw==} + valtio@2.3.2: + resolution: {integrity: sha512-YXhWQei9IN/ZDce9rhL3trCq9+vVq8M1gWmKVdP3YSZ2gxsmmNWVbxXwf9yG6ffu/dAvAD91nevg8xirGr4Dhg==} engines: {node: '>=12.20.0'} peerDependencies: - '@types/react': '>=16.8' - react: '>=16.8' + '@types/react': '>=18.0.0' + react: '>=18.0.0' peerDependenciesMeta: '@types/react': optional: true @@ -24143,7 +24138,7 @@ snapshots: srvx: 0.11.16 tinyglobby: 0.2.17 ufo: 1.6.4 - vitefu: 1.1.1(vite@8.0.16(@types/node@22.13.14)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0)) + vitefu: 1.1.1(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0)) xmlbuilder2: 4.0.3 zod: 4.4.3 optionalDependencies: @@ -24176,7 +24171,7 @@ snapshots: srvx: 0.11.16 tinyglobby: 0.2.17 ufo: 1.6.4 - vitefu: 1.1.1(vite@8.0.16(@types/node@22.13.14)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0)) + vitefu: 1.1.1(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0)) xmlbuilder2: 4.0.3 zod: 4.4.3 optionalDependencies: @@ -26886,10 +26881,6 @@ snapshots: dequal@2.0.3: {} - derive-valtio@0.1.0(valtio@1.12.0(@types/react@19.2.14)(react@19.2.6)): - dependencies: - valtio: 1.12.0(@types/react@19.2.14)(react@19.2.6) - destr@2.0.5: {} destroy@1.2.0: {} @@ -32412,7 +32403,7 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 - proxy-compare@2.5.1: {} + proxy-compare@3.0.1: {} proxy-from-env@1.1.0: {} @@ -34893,11 +34884,9 @@ snapshots: validate.io-function@1.0.2: {} - valtio@1.12.0(@types/react@19.2.14)(react@19.2.6): + valtio@2.3.2(@types/react@19.2.14)(react@19.2.6): dependencies: - derive-valtio: 0.1.0(valtio@1.12.0(@types/react@19.2.14)(react@19.2.6)) - proxy-compare: 2.5.1 - use-sync-external-store: 1.2.0(react@19.2.6) + proxy-compare: 3.0.1 optionalDependencies: '@types/react': 19.2.14 react: 19.2.6 @@ -35129,7 +35118,7 @@ snapshots: tsx: 4.22.4 yaml: 2.9.0 - vitefu@1.1.1(vite@8.0.16(@types/node@22.13.14)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0)): + vitefu@1.1.1(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0)): optionalDependencies: vite: 8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 44b5c9e1b6ae4..17cbf3f5abb4d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -43,7 +43,7 @@ catalog: tailwindcss: ^4.2.4 tsx: ^4.22.0 typescript: ~6.0.2 - valtio: ^1.12.0 + valtio: ^2.3.2 vite: ^8.0.16 vite-tsconfig-paths: ^6.1.1 vitest: ^4.1.4 From dba31df91d82da2897dc69e367c29e09a122d0c1 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:08:13 +0200 Subject: [PATCH 3/7] fix: scoped PAT creation form error messages are hidden (#48011) ## Problem When creating a new scoped PAT, if users didn't add at least one permission or have a misconfigured permission (no access selected), the form does not submit but no error message is shown. The UI looks broken. ## Solution This is because there's a zod validation happening but its messages are not displayed for permissions. The proper fix is to use react-hook-form field array. image image ## Summary by CodeRabbit * **Bug Fixes** * Improved permission selection and toggling behavior in the scoped access token flow. * Enhanced validation feedback for permission rows and action selections, keeping error states in sync after changes. * Updated error handling to surface permission-related messages more reliably. * **Refactor** * Reworked the permissions UI to use a more reliable control-based rendering approach for rows, selection changes, and error presentation. --- .../PermissionResourceSelector.tsx | 27 +-- .../Scoped/Form/Permissions/Permissions.tsx | 227 ++++++++++-------- .../Form/Permissions/Permissions.types.ts | 15 +- .../Scoped/NewScopedTokenSheet.tsx | 4 +- 4 files changed, 147 insertions(+), 126 deletions(-) diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/PermissionResourceSelector.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/PermissionResourceSelector.tsx index e0d823e77eea6..79955c9ebcedd 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/PermissionResourceSelector.tsx +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/PermissionResourceSelector.tsx @@ -1,5 +1,4 @@ import { Key, Plus } from 'lucide-react' -import { Path, PathValue } from 'react-hook-form' import { Button, Checkbox, @@ -14,30 +13,16 @@ import { PopoverTrigger, } from 'ui' -import { - PermissionResource, - PermissionResourceSelectorProps, - PermissionRow, - PermissionsFormValues, -} from './Permissions.types' -import { togglePermissionResource } from './Permissions.utils' +import { PermissionResourceSelectorProps, PermissionRow } from './Permissions.types' import { ACCESS_TOKEN_RESOURCES } from '@/components/interfaces/Account/AccessTokens/AccessToken.constants' -export const PermissionResourceSelector = ({ +export const PermissionResourceSelector = ({ open, onOpenChange, + onResourceToggled, permissionRows, - setValue, align = 'center', -}: PermissionResourceSelectorProps) => { - const handleToggleResource = (resource: PermissionResource) => { - const newRows = togglePermissionResource(permissionRows, resource) - setValue( - 'permissionRows' as Path, - newRows as PathValue> - ) - } - +}: PermissionResourceSelectorProps) => { return ( @@ -61,13 +46,13 @@ export const PermissionResourceSelector = handleToggleResource(resource)} + onSelect={() => onResourceToggled(resource)} className="text-foreground" >
handleToggleResource(resource)} + onCheckedChange={() => onResourceToggled(resource)} onClick={(e) => e.stopPropagation()} /> diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.tsx index f22efce175dde..1860d289d0854 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.tsx +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.tsx @@ -1,21 +1,39 @@ import { ChevronDown, RotateCcw, X } from 'lucide-react' -import { Path, PathValue } from 'react-hook-form' -import { Button, Checkbox, Popover, PopoverContent, PopoverTrigger, WarningIcon } from 'ui' +import { useFieldArray, useFormState } from 'react-hook-form' +import { + Button, + Checkbox, + FormControl, + FormField, + FormMessage, + Popover, + PopoverContent, + PopoverTrigger, + WarningIcon, +} from 'ui' +import { TokenFormValues } from '../../../AccessToken.schemas' import { PermissionResourceSelector } from './PermissionResourceSelector' -import { PermissionRow, PermissionsFormValues, PermissionsProps } from './Permissions.types' +import { PermissionsProps } from './Permissions.types' import { sortActions } from './Permissions.utils' import { ACCESS_TOKEN_RESOURCES } from '@/components/interfaces/Account/AccessTokens/AccessToken.constants' import { formatAccessText } from '@/components/interfaces/Account/AccessTokens/AccessToken.utils' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' -export const Permissions = ({ - setValue, - watch, +export const Permissions = ({ + control, resourceSearchOpen, setResourceSearchOpen, -}: PermissionsProps) => { - const permissionRows = (watch('permissionRows' as Path) || []) as PermissionRow[] +}: PermissionsProps) => { + const { + fields: permissionRows, + append, + remove, + } = useFieldArray({ + name: 'permissionRows', + control, + }) + const { errors } = useFormState({ control, name: 'permissionRows' }) return (
@@ -28,12 +46,7 @@ export const Permissions = { - setValue( - 'permissionRows' as Path, - [] as PathValue> - ) - }} + onClick={() => remove()} icon={} tooltip={{ content: { @@ -49,7 +62,13 @@ export const Permissions = { + const index = permissionRows.findIndex((p) => p.resource === resource.resource) + if (index > -1) { + return remove(index) + } + append(resource) + }} align="end" />
@@ -66,88 +85,101 @@ export const Permissions = r.resource === row.resource ) return ( -
-
-
-
-
- - {selectedResource?.title} - -
-
-
-
- {selectedResource && ( - - + { + const fieldValue = field.value || [] + + return ( +
+
+
+
+
+ + {selectedResource?.title} + +
+
+
+
+ {selectedResource && ( + + + + + + + +
+ {sortActions(selectedResource.actions).map((action) => ( + + ))} +
+
+
+ )} - - -
- {sortActions(selectedResource.actions).map((action) => ( - - ))} -
-
- - )} -
-
- {index < permissionRows.length - 1 &&
} -
+ className="p-1" + onClick={() => { + remove(index) + }} + icon={} + aria-label="Remove" + /> +
+
+
+ +
+ {index < permissionRows.length - 1 && ( +
+ )} +
+ ) + }} + /> ) })}
@@ -160,6 +192,11 @@ export const Permissions =
+ {errors.permissionRows?.message || errors.permissionRows?.root?.message ? ( +

+ {errors.permissionRows?.message || errors.permissionRows?.root?.message} +

+ ) : null}
) } diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.types.ts b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.types.ts index ffb26fe487aee..8b3c4a5d43947 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.types.ts +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/Form/Permissions/Permissions.types.ts @@ -1,4 +1,6 @@ -import { FieldValues, UseFormSetValue, UseFormWatch } from 'react-hook-form' +import { Control, FieldValues } from 'react-hook-form' + +import { TokenFormValues } from '../../../AccessToken.schemas' export interface PermissionResource { resource: string @@ -15,19 +17,16 @@ export interface PermissionsFormValues extends FieldValues { permissionRows?: PermissionRow[] } -export interface PermissionsProps< - TFormValues extends PermissionsFormValues = PermissionsFormValues, -> { - setValue: UseFormSetValue - watch: UseFormWatch +export interface PermissionsProps { + control: Control resourceSearchOpen: boolean setResourceSearchOpen: (open: boolean) => void } -export interface PermissionResourceSelectorProps { +export interface PermissionResourceSelectorProps { open: boolean onOpenChange: (open: boolean) => void permissionRows: PermissionRow[] - setValue: UseFormSetValue + onResourceToggled: (resource: PermissionResource) => void align?: 'center' | 'end' | 'start' } diff --git a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenSheet.tsx b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenSheet.tsx index 21cd176dbcd27..31efd63cd9b55 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenSheet.tsx +++ b/apps/studio/components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenSheet.tsx @@ -66,6 +66,7 @@ export const NewScopedTokenSheet = ({ }, mode: 'onChange', }) + const track = useTrack() const { mutate: createAccessToken, isPending } = useAccessTokenCreateMutation() @@ -303,8 +304,7 @@ export const NewScopedTokenSheet = ({ /> From 77953e7a1fe1e8a2b8c1232eeda0fd7e22ced597 Mon Sep 17 00:00:00 2001 From: seungjae Date: Fri, 17 Jul 2026 23:14:31 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat(studio):=20include=20enums=20and=20RLS?= =?UTF-8?q?=20policies=20in=20Schema=20Visualizer=20Cop=E2=80=A6=20(#46189?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend the "Copy as Markdown" feature in the Schema Visualizer to include Custom Types/Enums and Row Level Security (RLS) Policies in the generated output. Closes https://github.com/orgs/supabase/discussions/46108 ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Feature — extend Copy as Markdown to include enums and RLS policies ## What is the current behavior? The "Copy as Markdown" function in the Schema Visualizer generates a SCHEMA.md that only includes tables, columns, and entity relationships. Custom Types/Enums and RLS Policies are missing, making the output an incomplete representation of the schema. Related: https://github.com/orgs/supabase/discussions/46108 ## What is the new behavior? The generated markdown now includes two additional sections: **Custom Types / Enums:** - Lists each enum type with its ordered permitted values - Filtered by the currently selected schema **RLS Policies:** - Grouped by table for self-contained readability - Includes policy name, command (SELECT/INSERT/UPDATE/DELETE/ALL), roles, action (PERMISSIVE/RESTRICTIVE), USING expression, and WITH CHECK expression **Example output:** ```markdown ## Custom Types / Enums ### `order_status` `pending` | `processing` | `shipped` | `delivered` ## RLS Policies ### `orders` | Policy | Command | Roles | Action | USING | WITH CHECK | |--------|---------|-------|--------|-------|------------| | `users_own_orders` | SELECT | authenticated | PERMISSIVE | `auth.uid() = user_id` | — | ``` ## Additional context - 3 files changed: `Schemas.utils.ts`, `SchemaGraph.tsx`, `Schemas.utils.test.ts` - New utility functions `getEnumsAsMarkdown()` and `getPoliciesAsMarkdown()` with unit tests - Reuses existing `useEnumeratedTypesQuery` and `useDatabasePoliciesQuery` hooks - No breaking changes — existing markdown output is preserved, new sections are appended ## Summary by CodeRabbit * **New Features** * "Copy as Markdown" now includes enumerated types and database policies alongside existing schema/table content; policies are included with their rule details and grouped by table. * **Tests** * Added tests covering enum and policy markdown generation, including matching/non-matching schema cases and policy formatting. [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46189?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) --------- Co-authored-by: Gildas Garcia <1122076+djhi@users.noreply.github.com> --- .../Database/Schemas/SchemaGraph.tsx | 55 ++++++++++++- .../Database/Schemas/Schemas.utils.test.ts | 82 ++++++++++++++++++- .../Database/Schemas/Schemas.utils.ts | 61 ++++++++++++++ 3 files changed, 194 insertions(+), 4 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx b/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx index 1c6ac29bba8ea..2dcbf39311bbd 100644 --- a/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx +++ b/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx @@ -47,8 +47,10 @@ import { SchemaGraphContextProvider, SchemaGraphContextType } from './SchemaGrap import { SchemaGraphLegend } from './SchemaGraphLegend' import { EdgeData, TableNodeData } from './Schemas.constants' import { + getEnumsAsMarkdown, getGraphDataFromTables, getLayoutedElementsViaDagre, + getPoliciesAsMarkdown, getSchemaAsMarkdown, } from './Schemas.utils' import { TableNode } from './SchemaTableNode' @@ -57,7 +59,9 @@ import { AlertError } from '@/components/ui/AlertError' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { SchemaSelector } from '@/components/ui/SchemaSelector' import { Shortcut } from '@/components/ui/Shortcut' +import { useDatabasePoliciesQuery } from '@/data/database-policies/database-policies-query' import { useSchemasQuery } from '@/data/database/schemas-query' +import { useEnumeratedTypesQuery } from '@/data/enumerated-types/enumerated-types-query' import { useInfiniteTablesQuery } from '@/data/tables/tables-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useLocalStorage } from '@/hooks/misc/useLocalStorage' @@ -137,6 +141,20 @@ export const SchemaGraph = () => { const tables = useMemo(() => tablesData?.pages.flat() ?? [], [tablesData]) const hasNoTables = isSuccessTables && isSuccessSchemas && tables.length === 0 && !hasNextPage + const { data: enumeratedTypes = [], isPending: isLoadingEnumeratedTypes } = + useEnumeratedTypesQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const { data: policies = [], isPending: isLoadingPolicies } = useDatabasePoliciesQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + schemas: [selectedSchema], + }) + + const isMarkdownDataLoading = isLoadingEnumeratedTypes || isLoadingPolicies + const schema = (schemas ?? []).find((s) => s.name === selectedSchema) const [, setStoredPositions] = useLocalStorage( LOCAL_STORAGE_KEYS.SCHEMA_VISUALIZER_POSITIONS(ref as string, schema?.id ?? 0), @@ -224,11 +242,37 @@ export const SchemaGraph = () => { } const copyAsMarkdown = () => { + if (isMarkdownDataLoading) return + const tableNodes = reactFlowInstance .getNodes() .filter((node) => node.type === 'table') .map((node) => node.data as TableNodeData) - copyToClipboard(getSchemaAsMarkdown(selectedSchema, tableNodes)) + + let markdown = getSchemaAsMarkdown(selectedSchema, tableNodes) + + const enumsMarkdown = getEnumsAsMarkdown( + selectedSchema, + enumeratedTypes.map((e) => ({ name: e.name, schema: e.schema, enums: e.enums })) + ) + if (enumsMarkdown) markdown += enumsMarkdown + + const policiesMarkdown = getPoliciesAsMarkdown( + selectedSchema, + policies.map((p) => ({ + name: p.name, + schema: p.schema, + table: p.table, + command: p.command, + roles: p.roles, + action: p.action, + definition: p.definition ? String(p.definition) : null, + check: p.check ? String(p.check) : null, + })) + ) + if (policiesMarkdown) markdown += policiesMarkdown + + copyToClipboard(markdown) setCopied(true) toast.success('Successfully copied as Markdown') } @@ -246,7 +290,7 @@ export const SchemaGraph = () => { useShortcut(SHORTCUT_IDS.SCHEMA_VISUALIZER_COPY_SQL, copyAsSQL, { enabled: shortcutsEnabled }) useShortcut(SHORTCUT_IDS.SCHEMA_VISUALIZER_COPY_MARKDOWN, copyAsMarkdown, { - enabled: shortcutsEnabled, + enabled: shortcutsEnabled && !isMarkdownDataLoading, }) useShortcut(SHORTCUT_IDS.SCHEMA_VISUALIZER_DOWNLOAD_PNG, () => downloadImage('png'), { enabled: shortcutsEnabled, @@ -423,12 +467,17 @@ export const SchemaGraph = () => { { e.stopPropagation() copyAsMarkdown() }} > - + {isMarkdownDataLoading ? ( + + ) : ( + + )} Copy as Markdown { test('getSchemaAsMarkdown returns properly formatted markdown', () => { @@ -173,4 +178,79 @@ An excellent description | \`user_id\` | \`uuid\` | Nullable | `) }) + + test('getEnumsAsMarkdown returns formatted enum section', () => { + const enums = [ + { name: 'order_status', schema: 'public', enums: ['pending', 'processing', 'shipped'] }, + { name: 'role_type', schema: 'public', enums: ['admin', 'user'] }, + { name: 'other_enum', schema: 'auth', enums: ['a', 'b'] }, + ] + const result = getEnumsAsMarkdown('public', enums) + expect(result).toBe(`## Custom Types / Enums + +### \`order_status\` + +\`pending\` | \`processing\` | \`shipped\` + +### \`role_type\` + +\`admin\` | \`user\` + +`) + }) + + test('getEnumsAsMarkdown returns empty string when no enums match schema', () => { + const enums = [{ name: 'other_enum', schema: 'auth', enums: ['a', 'b'] }] + const result = getEnumsAsMarkdown('public', enums) + expect(result).toBe('') + }) + + test('getPoliciesAsMarkdown returns formatted policy table grouped by table', () => { + const policies = [ + { + name: 'users_select', + schema: 'public', + table: 'users', + command: 'SELECT', + roles: ['authenticated'], + action: 'PERMISSIVE', + definition: 'auth.uid() = id', + check: null, + }, + { + name: 'users_insert', + schema: 'public', + table: 'users', + command: 'INSERT', + roles: ['authenticated'], + action: 'PERMISSIVE', + definition: null, + check: 'auth.uid() = id', + }, + ] + const result = getPoliciesAsMarkdown('public', policies) + expect(result).toContain('## RLS Policies') + expect(result).toContain('### `users`') + expect(result).toContain('`users_select`') + expect(result).toContain('`users_insert`') + expect(result).toContain('`auth.uid() = id`') + expect(result).toContain('—') + }) + + test('getPoliciesAsMarkdown returns empty string when no policies match schema', () => { + const policies = [ + { + name: 'test', + schema: 'auth', + table: 'users', + command: 'SELECT', + roles: ['anon'], + action: 'PERMISSIVE', + definition: 'true', + check: null, + }, + ] + const result = getPoliciesAsMarkdown('public', policies) + expect(result).toBe('') + }) }) diff --git a/apps/studio/components/interfaces/Database/Schemas/Schemas.utils.ts b/apps/studio/components/interfaces/Database/Schemas/Schemas.utils.ts index 9b2d24d73a63d..c328f6c91fb8f 100644 --- a/apps/studio/components/interfaces/Database/Schemas/Schemas.utils.ts +++ b/apps/studio/components/interfaces/Database/Schemas/Schemas.utils.ts @@ -281,3 +281,64 @@ const escapeForMarkdown = (str: string) => { .replace(/\n/g, ' ') ) } + +// ── Enum / Custom Type markdown ──────────────────────────── + +export type EnumForMarkdown = { + name: string + schema: string + enums: string[] +} + +export const getEnumsAsMarkdown = (schema: string, enums: EnumForMarkdown[]): string => { + const filtered = enums.filter((e) => e.schema === schema && e.enums.length > 0) + if (filtered.length === 0) return '' + + let md = `## Custom Types / Enums\n\n` + for (const enumType of filtered) { + const values = enumType.enums.map((v) => `\`${escapeForMarkdown(v)}\``).join(' | ') + md += `### \`${escapeForMarkdown(enumType.name)}\`\n\n${values}\n\n` + } + return md +} + +// ── RLS Policy markdown ──────────────────────────────────── + +export type PolicyForMarkdown = { + name: string + schema: string + table: string + command: string + roles: string[] + action: string + definition: string | null + check: string | null +} + +export const getPoliciesAsMarkdown = (schema: string, policies: PolicyForMarkdown[]): string => { + const filtered = policies.filter((p) => p.schema === schema) + if (filtered.length === 0) return '' + + // Group by table + const byTable = new Map() + for (const policy of filtered) { + const existing = byTable.get(policy.table) ?? [] + existing.push(policy) + byTable.set(policy.table, existing) + } + + let md = `## RLS Policies\n\n` + for (const [table, tablePolicies] of byTable) { + md += `### \`${escapeForMarkdown(table)}\`\n\n` + md += `| Policy | Command | Roles | Action | USING | WITH CHECK |\n` + md += `|--------|---------|-------|--------|-------|------------|\n` + for (const p of tablePolicies) { + const roles = p.roles.map((r) => escapeForMarkdown(r)).join(', ') + const using = p.definition ? `\`${escapeForMarkdown(p.definition)}\`` : '—' + const check = p.check ? `\`${escapeForMarkdown(p.check)}\`` : '—' + md += `| \`${escapeForMarkdown(p.name)}\` | ${p.command} | ${roles} | ${p.action} | ${using} | ${check} |\n` + } + md += `\n` + } + return md +} From 50e9fedb2039c4ced412fe981612269e847a6239 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Sat, 18 Jul 2026 00:17:52 +1000 Subject: [PATCH 5/7] feat(studio): finesse logs date picker range colours (#48019) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? UI polish / bug fix for the shared Calendar range selection and Logs date picker. ## What is the current behavior? - Selected date ranges use opaque `brand-400` / `brand-500` fills that read too loud in light mode, with black text that is hard to read on darker endpoints. - Start/end days are squared off on the connecting edge without intentional outer rounding. - Outside days in a selected range are dimmed with `opacity-50`, which can tint the range wash incorrectly when a range starts in the prior month. - The large-range warning in `LogsDatePicker` is a full-bleed yellow banner that feels too heavy for the popover. - Time inputs show a clock icon that adds visual noise. ## What is the new behavior? - Range middle uses a softer `brand-200` wash; start/end stay on stronger brand fills with readable foreground text. - Start days round on the left (`rounded-l-md`), end days on the right (`rounded-r-md`); day hover keeps `rounded-md`. - Selected outside days and “today” no longer fight the range wash colours. - Large-range warning is quiet inline `text-warning` copy that wraps to the calendar column width. - Clock icon removed from `TimeSplitInput`. | Before | After | | --- | --- | | CleanShot 2026-07-16 at 17 59
21@2x | CleanShot 2026-07-16 at 17 59
34@2x | ## Additional context Shared `Calendar` changes apply anywhere range mode is used, not only logs. ## Summary by CodeRabbit - **Style** - Refined the date-picker popover layout for start/end controls with better fit and max-width handling. - Updated calendar day and range visuals (selection, outside states, rounding, and hover behavior) to reduce “ghost” styling and improve consistency. - Restyled the large-range warning to improve spacing and alignment. - Simplified the time-splitting input UI by removing the leading clock icon. - Adjusted the “Copy range” button feedback color for copied/pasted states. --- .../Settings/Logs/Logs.DatePickers.tsx | 12 +++---- .../ui/DatePicker/TimeSplitInput.tsx | 5 --- .../ui/src/components/shadcn/ui/calendar.tsx | 35 ++++++++++++++----- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx b/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx index 7da4fab326e00..1c1c6975f5289 100644 --- a/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx @@ -403,7 +403,7 @@ export const LogsDatePicker = ({ -
+
-
+
{isLargeRange && !hideWarnings && ( -
- Large ranges may result in memory errors for
big projects. -
+

+ Large ranges may result in memory errors for big projects. +

)}
{startDate && endDate ? ( @@ -470,7 +470,7 @@ export const LogsDatePicker = ({ size="tiny" onClick={handleCopy} className={cn({ - 'text-brand-600': copied || pasted, + 'text-brand-link': copied || pasted, })} > {copied ? 'Copied!' : pasted ? 'Pasted!' : 'Copy range'} diff --git a/apps/studio/components/ui/DatePicker/TimeSplitInput.tsx b/apps/studio/components/ui/DatePicker/TimeSplitInput.tsx index 6851e41d356a5..e2337a14ddc6d 100644 --- a/apps/studio/components/ui/DatePicker/TimeSplitInput.tsx +++ b/apps/studio/components/ui/DatePicker/TimeSplitInput.tsx @@ -1,5 +1,4 @@ import { format } from 'date-fns' -import { Clock } from 'lucide-react' import { useEffect, useState } from 'react' import { cn } from 'ui' @@ -235,10 +234,6 @@ export const TimeSplitInput = ({ hover:border-stronger transition-colors `} > -
- -
- handleOnBlur()} diff --git a/packages/ui/src/components/shadcn/ui/calendar.tsx b/packages/ui/src/components/shadcn/ui/calendar.tsx index ced618435f56f..05fa59a7bdd2d 100644 --- a/packages/ui/src/components/shadcn/ui/calendar.tsx +++ b/packages/ui/src/components/shadcn/ui/calendar.tsx @@ -69,23 +69,42 @@ function Calendar({ className, classNames, showOutsideDays = true, ...props }: C weekday: cn('text-foreground-muted rounded-md w-9 font-normal text-[0.8rem]', weekday), week: cn('flex w-full mt-2', week), day: cn( - 'text-center text-sm p-0 relative [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md', - 'last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20', + 'text-center text-sm p-0 relative', + 'first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md', + 'focus-within:relative focus-within:z-20', 'w-9 box-border', day ), day_button: cn( buttonVariants({ variant: 'ghost' }), - 'h-9 w-9 p-0 font-normal aria-selected:opacity-100', + 'h-9 w-9 p-0 font-normal rounded-md aria-selected:opacity-100', + // Keep selected days from picking up ghost hover bg/text + 'aria-selected:hover:bg-transparent aria-selected:hover:text-inherit', day_button ), - selected: cn('bg-brand-500 text-foreground text-foreground', selected), + selected: cn( + !fullDateRangeSelected && 'bg-brand-400! dark:bg-brand-500! text-foreground rounded-md', + selected + ), + // Plain accent — range/selected fills use ! so they still win when today is in the selection today: cn('bg-accent text-accent-foreground', today), - outside: cn('text-foreground-muted opacity-50', outside), + outside: cn( + 'text-foreground-muted opacity-50 has-[[aria-selected]]:opacity-100 has-[[aria-selected]]:text-foreground', + outside + ), disabled: cn('text-foreground-muted opacity-50', disabled), - range_start: cn(fullDateRangeSelected && 'bg-brand-500 rounded-r-none', range_start), - range_middle: cn('aria-selected:bg-brand-400 rounded-none', range_middle), - range_end: cn(fullDateRangeSelected && 'bg-brand-500 rounded-l-none', range_end), + range_start: cn( + fullDateRangeSelected && 'bg-brand-400! dark:bg-brand-500! text-foreground rounded-l-md', + range_start + ), + range_middle: cn( + 'bg-brand-200! dark:bg-brand-400! text-foreground rounded-none', + range_middle + ), + range_end: cn( + fullDateRangeSelected && 'bg-brand-400! dark:bg-brand-500! text-foreground rounded-r-md', + range_end + ), hidden: cn('invisible', hidden), ...restClassNames, }} From 6f19dfe18c18bdd8dec7f08e66e16389b8b04f28 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Sat, 18 Jul 2026 00:18:45 +1000 Subject: [PATCH 6/7] fix(ui): bake explicit tabindex into interactive primitives (#47984) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? A11y fix for keyboard focus (esp. Safari), plus a lint rule to keep it from regressing. ## What is the current behavior? `Button` already defaults an explicit `tabIndex={0}` (#40458). Other interactive primitives (Checkbox, bare triggers, etc.) still skip Tab focus in Safari unless macOS Keyboard navigation is on. Raw ` + *
Save
+ * + * GOOD: + * + * + * + */ + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: + 'Require explicit tabIndex on raw