mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 03:15:06 +03:00
chore(studio): generate apps/studio/AGENTS.md for CodeRabbit from skills
CodeRabbit's code-guidelines feature auto-detects guideline files and directory-scopes them: a file applies only to its own directory and below. Our Studio conventions live in .claude/skills/, which contains no code, so CodeRabbit would never apply them to apps/studio. AGENTS.md is the right carrier: CodeRabbit auto-detects **/AGENTS.md, and apps/studio/AGENTS.md scopes correctly to apps/studio/**. Unlike CLAUDE.md, Claude Code does not read AGENTS.md, so this adds zero context cost — Claude keeps using the skills directly, and they stay the single source of truth. A generator inlines the relevant skills into apps/studio/AGENTS.md (a derived artifact) so there is no maintenance duplication; CI fails if it drifts.
This commit is contained in:
1 parent
f6afd8b2e4
commit
1f4e59ddb0
5 files changed
+727
No files matched your search
@@ -0,0 +1,39 @@
|
||||
name: Studio Guidelines Sync
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
paths:
|
||||
- '.claude/skills/**'
|
||||
- 'apps/studio/AGENTS.md'
|
||||
- 'scripts/sync-studio-guidelines.mjs'
|
||||
|
||||
# Cancel old builds on new commit for same workflow + branch/PR
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check apps/studio/AGENTS.md is in sync
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
.claude/skills
|
||||
apps/studio/AGENTS.md
|
||||
scripts
|
||||
.nvmrc
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- name: Verify guidelines are up to date
|
||||
run: node scripts/sync-studio-guidelines.mjs --check
|
||||
@@ -21,6 +21,8 @@ apps/studio/public
|
||||
apps/**/.turbo
|
||||
apps/docs/CONTRIBUTING.md
|
||||
apps/docs/__generated__
|
||||
# Generated by scripts/sync-studio-guidelines.mjs from .claude/skills/
|
||||
apps/studio/AGENTS.md
|
||||
apps/design-system/__registry__
|
||||
# TanStack Router auto-generated route tree; the file header explicitly
|
||||
# says to exclude it from formatters.
|
||||
|
||||
@@ -0,0 +1,585 @@
|
||||
<!--
|
||||
GENERATED FILE — DO NOT EDIT.
|
||||
|
||||
Source of truth: the Studio skills in .claude/skills/ (listed below).
|
||||
Regenerate with: pnpm sync:guidelines
|
||||
CI fails if this file drifts from the skills (studio-guidelines-sync workflow).
|
||||
|
||||
This file exists so CodeRabbit applies the Studio conventions when reviewing
|
||||
apps/studio code. CodeRabbit auto-detects **/AGENTS.md and scopes it to this
|
||||
directory tree. Claude Code does not read AGENTS.md and uses the skills directly.
|
||||
-->
|
||||
|
||||
# Studio Code Review Guidelines
|
||||
|
||||
Conventions for `apps/studio` code, compiled from the Studio skills. Apply these
|
||||
when reviewing changes under `apps/studio/`.
|
||||
|
||||
<!-- source: .claude/skills/studio-best-practices/SKILL.md -->
|
||||
|
||||
# Studio Best Practices
|
||||
|
||||
Applies to `apps/studio/**/*.{ts,tsx}`.
|
||||
|
||||
## Boolean Naming
|
||||
|
||||
Use descriptive prefixes — derive from existing state rather than storing separately:
|
||||
|
||||
- `is` — state/identity: `isLoading`, `isPaused`, `isNewRecord`
|
||||
- `has` — possession: `hasPermission`, `hasData`
|
||||
- `can` — capability: `canUpdateColumns`, `canDelete`
|
||||
- `should` — conditional behavior: `shouldFetch`, `shouldRender`
|
||||
|
||||
Extract complex conditions into named variables:
|
||||
|
||||
```tsx
|
||||
// ❌ inline multi-condition
|
||||
{
|
||||
!isSchemaLocked && isTableLike(selectedTable) && canUpdateColumns && !isLoading && <Button />
|
||||
}
|
||||
|
||||
// ✅ named variable
|
||||
const canShowAddButton =
|
||||
!isSchemaLocked && isTableLike(selectedTable) && canUpdateColumns && !isLoading
|
||||
{
|
||||
canShowAddButton && <Button />
|
||||
}
|
||||
```
|
||||
|
||||
Derive booleans — don't store them:
|
||||
|
||||
```tsx
|
||||
// ❌ stored derived state
|
||||
const [isFormValid, setIsFormValid] = useState(false)
|
||||
useEffect(() => {
|
||||
setIsFormValid(name.length > 0 && email.includes('@'))
|
||||
}, [name, email])
|
||||
|
||||
// ✅ derived
|
||||
const isFormValid = name.length > 0 && email.includes('@')
|
||||
```
|
||||
|
||||
## Component Structure
|
||||
|
||||
See `vercel-composition-patterns` skill for compound component and composition patterns.
|
||||
|
||||
Keep components under 200–300 lines. Split when you see:
|
||||
|
||||
- Multiple distinct UI sections
|
||||
- Complex conditional rendering
|
||||
- Multiple unrelated `useState` calls
|
||||
- Hard to understand at a glance
|
||||
|
||||
Co-locate sub-components in the same directory as the parent. Avoid barrel re-export files.
|
||||
|
||||
Extract repeated JSX patterns into small components.
|
||||
|
||||
## Data Fetching
|
||||
|
||||
All data fetching uses TanStack Query (React Query). See `studio-queries` skill for query/mutation patterns and `studio-error-handling` skill for error display conventions.
|
||||
|
||||
### Loading / Error / Success Pattern
|
||||
|
||||
Top level:
|
||||
|
||||
```tsx
|
||||
const { data, error, isLoading, isError, isSuccess } = useQuery(...)
|
||||
|
||||
if (isLoading) return <GenericSkeletonLoader />
|
||||
if (isError) return <AlertError error={error} subject="Failed to load data" />
|
||||
if (isSuccess && data.length === 0) return <EmptyState />
|
||||
return <DataDisplay data={data} />
|
||||
```
|
||||
|
||||
Use early returns — avoid deeply nested conditionals.
|
||||
|
||||
Inline:
|
||||
|
||||
```tsx
|
||||
<div>
|
||||
{isLoading && <InlineLoader />}
|
||||
{isError && <InlineError error={error} />}
|
||||
{isSuccess && data.length === 0 && <EmptyState />}
|
||||
{isSuccess && data.length > 0 && <DataDisplay data={data} />}
|
||||
</div>
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
Keep state as local as possible; lift only when needed.
|
||||
|
||||
Group related form state with `react-hook-form` rather than multiple `useState` calls. See `studio-ui-patterns` skill for form layout and component conventions.
|
||||
|
||||
```tsx
|
||||
// ❌ multiple related useState
|
||||
const [name, setName] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
|
||||
// ✅ grouped with react-hook-form
|
||||
const form = useForm<FormValues>({ defaultValues: { name: '', email: '' } })
|
||||
```
|
||||
|
||||
## Custom Hooks
|
||||
|
||||
Extract complex or reusable logic into hooks. Return objects, not arrays:
|
||||
|
||||
```tsx
|
||||
// ❌ array return (hard to extend)
|
||||
return [value, toggle]
|
||||
|
||||
// ✅ object return
|
||||
return { value, toggle, setTrue, setFalse }
|
||||
```
|
||||
|
||||
## Event Handlers
|
||||
|
||||
- Prop callbacks: `on` prefix (`onClose`, `onSave`)
|
||||
- Internal handlers: `handle` prefix (`handleSubmit`, `handleCancel`)
|
||||
|
||||
Use `useCallback` for handlers passed to memoized children; avoid unnecessary inline arrow functions.
|
||||
|
||||
## Conditional Rendering
|
||||
|
||||
```tsx
|
||||
// Simple show/hide
|
||||
<>{isVisible && <Component />}</>
|
||||
|
||||
// Binary choice
|
||||
<>{isLoading ? <Spinner /> : <Content />}</>
|
||||
|
||||
// Multiple conditions — use early returns, not nested ternaries
|
||||
if (isLoading) return <Spinner />
|
||||
if (isError) return <Error />
|
||||
return <Content />
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
`useMemo` for genuinely expensive computations (measured, not assumed). Don't wrap everything — only optimize when you have a measured problem or are passing values to memoized children.
|
||||
|
||||
## TypeScript
|
||||
|
||||
Define prop interfaces explicitly. Use discriminated unions for complex state:
|
||||
|
||||
```tsx
|
||||
type AsyncState<T> =
|
||||
| { status: 'idle' }
|
||||
| { status: 'loading' }
|
||||
| { status: 'success'; data: T }
|
||||
| { status: 'error'; error: Error }
|
||||
```
|
||||
|
||||
Avoid `as any` / `as Type` casts. Validate at boundaries with zod:
|
||||
|
||||
```tsx
|
||||
// ❌ type cast
|
||||
const user = apiResponse as User
|
||||
|
||||
// ✅ zod parse
|
||||
const user = userSchema.parse(apiResponse)
|
||||
// or safe:
|
||||
const result = userSchema.safeParse(apiResponse)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Extract logic into `.utils.ts` pure functions and test exhaustively. See the `studio-testing` skill for the full testing strategy and decision tree.
|
||||
|
||||
---
|
||||
|
||||
<!-- source: .claude/skills/studio-ui-patterns/SKILL.md -->
|
||||
|
||||
# Studio UI Patterns
|
||||
|
||||
The Design System docs and demos are the source of truth. Always check the relevant
|
||||
demo file before composing new UI.
|
||||
|
||||
## Layout
|
||||
|
||||
Docs: `apps/design-system/content/docs/ui-patterns/layout.mdx`
|
||||
|
||||
Build pages with `PageContainer`, `PageHeader`, and `PageSection`.
|
||||
|
||||
| Content type | `size` |
|
||||
| ----------------- | ----------- |
|
||||
| Settings / config | `"default"` |
|
||||
| Lists / tables | `"large"` |
|
||||
| Full-screen views | `"full"` |
|
||||
|
||||
- If filters/search exist on a list page, align table actions with the filters (don't use `PageHeaderAside`/`PageSectionAside` for those actions)
|
||||
- If no filters, actions can go in `PageHeaderAside` or `PageSectionAside`
|
||||
|
||||
Demos: `page-layout-settings.tsx`, `page-layout-list.tsx`, `page-layout-list-simple.tsx`, `page-layout-detail.tsx`
|
||||
(all in `apps/design-system/registry/default/example/`)
|
||||
|
||||
## Forms
|
||||
|
||||
Docs: `apps/design-system/content/docs/ui-patterns/forms.mdx`
|
||||
|
||||
- Use `react-hook-form` + `zod`
|
||||
- Use `FormItemLayout` instead of manually composing `FormItem`/`FormLabel`/`FormMessage`/`FormDescription`
|
||||
- Wrap inputs with `FormControl`; use `_Shadcn_` imports from `ui` for primitives
|
||||
|
||||
Layout selection:
|
||||
|
||||
| Context | Layout | Container |
|
||||
| ------------------------------------------ | ------------------------------------------ | ---------------------------------------------------------- |
|
||||
| Page (settings/config) | `FormItemLayout layout="flex-row-reverse"` | `Card` (`CardContent` per field; `CardFooter` for actions) |
|
||||
| Side panel — wide | `FormItemLayout layout="horizontal"` | `SheetSection` |
|
||||
| Side panel — narrow (`size="sm"` or below) | `FormItemLayout layout="vertical"` | `SheetSection` |
|
||||
|
||||
Dirty state / submit:
|
||||
|
||||
- Destructure `isDirty` from `form.formState` to show Cancel and disable Save
|
||||
- Show loading on submit button via `loading` prop
|
||||
- If submit button is outside `<form>`, set a stable `formId` and use `form` prop on the button
|
||||
|
||||
Demos: `form-patterns-pagelayout.tsx`, `form-patterns-sidepanel.tsx`
|
||||
|
||||
## Tables
|
||||
|
||||
Docs: `apps/design-system/content/docs/ui-patterns/tables.mdx`
|
||||
|
||||
| Pattern | Use when |
|
||||
| ---------- | ----------------------------------------------------------------------- |
|
||||
| `Table` | Simple, static, semantic display |
|
||||
| Data Table | TanStack-powered; sorting, filtering, pagination; composed per use-case |
|
||||
| Data Grid | Virtualization, column resizing, or complex cell editing |
|
||||
|
||||
- Actions: above the table, aligned right
|
||||
- Search/filters: above the table, aligned left
|
||||
- If table is primary content with no filters, actions can live in the page's primary/secondary actions area
|
||||
|
||||
Demos: `table-demo.tsx`, `data-table-demo.tsx`, `data-grid-demo.tsx`
|
||||
|
||||
## Charts
|
||||
|
||||
Docs: `apps/design-system/content/docs/ui-patterns/charts.mdx`
|
||||
|
||||
- Use provided chart building blocks; avoid passing raw Recharts components to `ChartContent`
|
||||
- Use `useChart` context flags for loading/disabled states
|
||||
- Keep composition straightforward — avoid over-abstraction
|
||||
|
||||
Demos (in `apps/design-system/__registry__/default/block/`): `chart-composed-demo.tsx`, `chart-composed-basic.tsx`, `chart-composed-states.tsx`, `chart-composed-metrics.tsx`, `chart-composed-actions.tsx`, `chart-composed-table.tsx`
|
||||
|
||||
## Empty States
|
||||
|
||||
Docs: `apps/design-system/content/docs/ui-patterns/empty-states.mdx`
|
||||
|
||||
| Scenario | Pattern |
|
||||
| ------------------------ | ------------------------------------------------------------------- |
|
||||
| Initial / onboarding | Presentational empty state with value prop + clear next action |
|
||||
| Data-heavy lists | Informational empty state matching the list/table layout |
|
||||
| Zero results from search | Keep layout consistent with data state to avoid jarring transitions |
|
||||
| Missing route | Centered `Admonition` |
|
||||
|
||||
Demos: `empty-state-presentational-icon.tsx`, `empty-state-initial-state-informational.tsx`, `empty-state-zero-items-table.tsx`, `data-grid-empty-state.tsx`, `empty-state-missing-route.tsx`
|
||||
|
||||
## Navigation
|
||||
|
||||
Docs: `apps/design-system/content/docs/ui-patterns/navigation.mdx`
|
||||
|
||||
- Use `NavMenu` for a horizontal list of related views within a consistent page layout
|
||||
- Activating an item must trigger a **URL change** — no local-only tab state
|
||||
|
||||
## Cards
|
||||
|
||||
- Group related information in cards
|
||||
- `CardContent` for sections, `CardFooter` for actions
|
||||
- Only use `CardHeader`/`CardTitle` when context isn't already provided by surrounding content
|
||||
- Use headers/titles when multiple cards represent distinct groups (e.g. multiple settings sections)
|
||||
|
||||
## Alerts
|
||||
|
||||
- Use `Admonition` to call out important actions, restrictions, or critical context
|
||||
- Place at the **top of a page's content** (below page title) or **top of the relevant section** (below section title)
|
||||
- Use sparingly
|
||||
|
||||
## Sheets (Side Panels)
|
||||
|
||||
Use a `Sheet` when switching pages would be disruptive and the user needs to maintain context (e.g. selecting a row from a list to edit).
|
||||
|
||||
Structure:
|
||||
|
||||
- `SheetContent` with `size="lg"` for forms needing horizontal layout
|
||||
- Use `SheetHeader`, `SheetTitle`, `SheetSection`, `SheetFooter`
|
||||
- Submit/cancel actions go in `SheetFooter`
|
||||
|
||||
Forms in sheets:
|
||||
|
||||
- `layout="horizontal"` for wider sheets
|
||||
- `layout="vertical"` for narrow sheets (`size="sm"` or below)
|
||||
|
||||
---
|
||||
|
||||
<!-- source: .claude/skills/vercel-composition-patterns/SKILL.md -->
|
||||
|
||||
# React Composition Patterns
|
||||
|
||||
Composition patterns for building flexible, maintainable React components. Avoid
|
||||
boolean prop proliferation by using compound components, lifting state, and
|
||||
composing internals. These patterns make codebases easier for both humans and AI
|
||||
agents to work with as they scale.
|
||||
|
||||
## When to Apply
|
||||
|
||||
Reference these guidelines when:
|
||||
|
||||
- Refactoring components with many boolean props
|
||||
- Building reusable component libraries
|
||||
- Designing flexible component APIs
|
||||
- Reviewing component architecture
|
||||
- Working with compound components or context providers
|
||||
|
||||
## Rule Categories by Priority
|
||||
|
||||
| Priority | Category | Impact | Prefix |
|
||||
| -------- | ----------------------- | ------ | --------------- |
|
||||
| 1 | Component Architecture | HIGH | `architecture-` |
|
||||
| 2 | State Management | MEDIUM | `state-` |
|
||||
| 3 | Implementation Patterns | MEDIUM | `patterns-` |
|
||||
| 4 | React 19 APIs | MEDIUM | `react19-` |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### 1. Component Architecture (HIGH)
|
||||
|
||||
- `architecture-avoid-boolean-props` - Don't add boolean props to customize
|
||||
behavior; use composition
|
||||
- `architecture-compound-components` - Structure complex components with shared
|
||||
context
|
||||
|
||||
### 2. State Management (MEDIUM)
|
||||
|
||||
- `state-decouple-implementation` - Provider is the only place that knows how
|
||||
state is managed
|
||||
- `state-context-interface` - Define generic interface with state, actions, meta
|
||||
for dependency injection
|
||||
- `state-lift-state` - Move state into provider components for sibling access
|
||||
|
||||
### 3. Implementation Patterns (MEDIUM)
|
||||
|
||||
- `patterns-explicit-variants` - Create explicit variant components instead of
|
||||
boolean modes
|
||||
- `patterns-children-over-render-props` - Use children for composition instead
|
||||
of renderX props
|
||||
|
||||
### 4. React 19 APIs (MEDIUM)
|
||||
|
||||
> **⚠️ React 19+ only.** Skip these patterns if you're on React 18 or earlier.
|
||||
|
||||
- `react19-no-forwardref` - Don't use `forwardRef`; use `use()` instead of `useContext()`
|
||||
|
||||
## How to Use
|
||||
|
||||
Read individual rule files for detailed explanations and code examples:
|
||||
|
||||
```
|
||||
rules/architecture-avoid-boolean-props.md
|
||||
rules/state-context-interface.md
|
||||
```
|
||||
|
||||
Each rule file contains:
|
||||
|
||||
- Brief explanation of why it matters
|
||||
- Incorrect code example with explanation
|
||||
- Correct code example with explanation
|
||||
- Additional context and references
|
||||
|
||||
## Full Compiled Document
|
||||
|
||||
For the complete guide with all rules expanded: `AGENTS.md`
|
||||
|
||||
---
|
||||
|
||||
<!-- source: .claude/skills/studio-queries/SKILL.md -->
|
||||
|
||||
# Studio Queries & Mutations (React Query)
|
||||
|
||||
Follow the patterns in `apps/studio/data/`. Reference examples:
|
||||
|
||||
- Query options: `apps/studio/data/table-editor/table-editor-query.ts`
|
||||
- Mutation hook: `apps/studio/data/edge-functions/edge-functions-update-mutation.ts`
|
||||
- Keys: `apps/studio/data/edge-functions/keys.ts`
|
||||
|
||||
## Query Keys
|
||||
|
||||
Define a `keys.ts` per domain. Export `*Keys` helpers using array keys with `as const`. Never inline query keys in components.
|
||||
|
||||
```ts
|
||||
export const edgeFunctionsKeys = {
|
||||
list: (projectRef: string | undefined) => ['projects', projectRef, 'edge-functions'] as const,
|
||||
detail: (projectRef: string | undefined, slug: string | undefined) =>
|
||||
['projects', projectRef, 'edge-function', slug, 'detail'] as const,
|
||||
}
|
||||
```
|
||||
|
||||
## Query Options (preferred pattern)
|
||||
|
||||
Use `queryOptions` from `@tanstack/react-query`. This gives type safety and works with both `useQuery()` and `queryClient.fetchQuery()`.
|
||||
|
||||
Rules:
|
||||
|
||||
- Export `XVariables`, `XData`, and `XError` types (prefixed with the domain name)
|
||||
- Implement a **private** `getX(variables, signal?)` function:
|
||||
- Throws if required variables are missing
|
||||
- Passes `signal` for cancellation
|
||||
- Calls `handleError(error)` on failure (which throws); returns `data` on success
|
||||
- Not exported — use `queryClient.fetchQuery(xQueryOptions(...))` for imperative fetching
|
||||
- Export `xQueryOptions()` using `queryOptions`
|
||||
- Gate with `enabled` so the query doesn't run until required variables exist
|
||||
- Platform-only queries: include `IS_PLATFORM` from `lib/constants` in `enabled`
|
||||
- Don't add extra params to `xQueryOptions` — callers override by destructuring: `{ ...xQueryOptions(vars), enabled: true }`
|
||||
|
||||
```ts
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { xKeys } from './keys'
|
||||
import { get, handleError } from '@/data/fetchers'
|
||||
import { IS_PLATFORM } from '@/lib/constants'
|
||||
import { ResponseError } from '@/types'
|
||||
|
||||
export type XVariables = { projectRef?: string }
|
||||
export type XError = ResponseError
|
||||
|
||||
async function getX({ projectRef }: XVariables, signal?: AbortSignal) {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
const { data, error } = await get('/v1/projects/{ref}/x', {
|
||||
params: { path: { ref: projectRef } },
|
||||
signal,
|
||||
})
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
export type XData = Awaited<ReturnType<typeof getX>>
|
||||
|
||||
export const xQueryOptions = ({ projectRef }: XVariables) =>
|
||||
queryOptions({
|
||||
queryKey: xKeys.list(projectRef),
|
||||
queryFn: ({ signal }) => getX({ projectRef }, signal),
|
||||
enabled: IS_PLATFORM && typeof projectRef !== 'undefined',
|
||||
})
|
||||
```
|
||||
|
||||
## Using Query Options in Components
|
||||
|
||||
```ts
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
import { xQueryOptions } from '@/data/x/x-query'
|
||||
|
||||
const { data, isPending, isError } = useQuery(xQueryOptions({ projectRef: project?.ref }))
|
||||
```
|
||||
|
||||
## Imperative Fetching (outside React or in callbacks)
|
||||
|
||||
```ts
|
||||
const queryClient = useQueryClient()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
|
||||
const handleClick = useCallback(
|
||||
async (id: number) => {
|
||||
const data = await queryClient.fetchQuery(xQueryOptions({ id, projectRef: project?.ref }))
|
||||
// use data...
|
||||
},
|
||||
[project?.ref, queryClient]
|
||||
)
|
||||
```
|
||||
|
||||
## Mutation Hook
|
||||
|
||||
- Export a `Variables` type with `projectRef`, identifiers, and `payload`
|
||||
- Implement a private `updateX(vars)` function with required variable validation and `handleError`
|
||||
- Wrap in `useXMutation()`:
|
||||
- Accepts `UseMutationOptions` (omit `mutationFn`)
|
||||
- Invalidates `list()` + `detail()` keys in `onSuccess` with `await Promise.all([...])`
|
||||
- Defaults to `toast.error(...)` when `onError` isn't provided
|
||||
|
||||
```ts
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { xKeys } from './keys'
|
||||
|
||||
type XUpdateVariables = { projectRef: string; slug: string; payload: XPayload }
|
||||
|
||||
export const useXUpdateMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: UseMutationOptions<XData, XError, XUpdateVariables> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: updateX,
|
||||
async onSuccess(data, variables, context) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: xKeys.detail(variables.projectRef, variables.slug),
|
||||
}),
|
||||
queryClient.invalidateQueries({ queryKey: xKeys.list(variables.projectRef) }),
|
||||
])
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(error, variables, context) {
|
||||
if (onError === undefined) toast.error(`Failed to update: ${error.message}`)
|
||||
else onError(error, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Component Usage
|
||||
|
||||
- Use React Query v5 flags: `isPending` for initial load, `isFetching` for background refetches
|
||||
- Render states explicitly in order: pending → error → success
|
||||
|
||||
---
|
||||
|
||||
<!-- source: .claude/skills/studio-error-handling/SKILL.md -->
|
||||
|
||||
# Studio Error Handling Pattern
|
||||
|
||||
Full docs and code examples: `apps/studio/components/interfaces/ErrorHandling/README.md`
|
||||
|
||||
## How it works
|
||||
|
||||
Classification happens in the **data layer**: `handleError` in `data/fetchers.ts` tests the error message against `ERROR_PATTERNS` and throws the matching error subclass (e.g. `ConnectionTimeoutError extends ResponseError`). The component (`ErrorMatcher`) reads `errorType` from the instance and does an O(1) lookup — it never does regex matching.
|
||||
|
||||
```
|
||||
handleError() → throws ConnectionTimeoutError → React Query catches → ErrorMatcher reads errorType → renders troubleshooting
|
||||
```
|
||||
|
||||
## Key files
|
||||
|
||||
| File | Purpose |
|
||||
| ------------------------------------- | ---------------------------------------------------------------- |
|
||||
| `data/error-patterns.ts` | Array of `{ pattern, ErrorClass }` — the regex lives here |
|
||||
| `types/api-errors.ts` | Error classes, `KnownErrorType` union, `ClassifiedError` type |
|
||||
| `ErrorMatcher.tsx` | Component — reads `errorType`, looks up mapping, renders |
|
||||
| `error-mappings.tsx` | `Record<KnownErrorType, { id, Troubleshooting: ComponentType }>` |
|
||||
| `errorMappings/ConnectionTimeout.tsx` | Reference troubleshooting component |
|
||||
| `TroubleshootingSections.tsx` | Reusable accordion section components |
|
||||
| `TroubleshootingAccordion.tsx` | Accordion wrapper with telemetry |
|
||||
|
||||
## Usage
|
||||
|
||||
Pass the **full error object** from React Query — not `error.message`:
|
||||
|
||||
```tsx
|
||||
{
|
||||
isError && (
|
||||
<ErrorMatcher title="Failed to load tables" error={error} supportFormParams={{ projectRef }} />
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## What NOT to do
|
||||
|
||||
- Do not pass `error.message` to `ErrorMatcher` — pass the full `error` object so the class is preserved.
|
||||
- Do not put regex patterns in `error-mappings.tsx` — they belong in `data/error-patterns.ts`.
|
||||
- Do not use `Object.assign` to stamp `errorType` — throw a proper subclass instead.
|
||||
- Do not pass a raw URL string for support — use `supportFormParams={{ projectRef }}`.
|
||||
- Do not put the page title inside the error mapping — it belongs on the `<ErrorMatcher>` caller.
|
||||
- Do not add callback props (`onDebugWithAI`, `onRestartProject`) to troubleshooting components — use hooks inside them instead.
|
||||
@@ -38,6 +38,7 @@
|
||||
"perf:meta": "ab -t 5 -c 20 -T application/json http://localhost:5555/tables",
|
||||
"setup:cli": "supabase start -x studio && supabase status --output json > keys.json && node scripts/generateLocalEnv.js",
|
||||
"generate:types": "supabase gen types typescript --local > ./supabase/functions/common/database-types.ts",
|
||||
"sync:guidelines": "node scripts/sync-studio-guidelines.mjs",
|
||||
"api:codegen": "cd packages/api-types && pnpm run codegen",
|
||||
"knip": "pnpx knip@~5.50.0",
|
||||
"authorize-vercel-deploys": "tsx scripts/authorizeVercelDeploys.ts"
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* Generates apps/studio/AGENTS.md from the canonical Studio skills in
|
||||
* .claude/skills/. The skills are the single source of truth; this file is a
|
||||
* derived artifact whose only consumer is CodeRabbit's code-guidelines feature,
|
||||
* which auto-detects `**\/AGENTS.md` and scopes a file to its own directory tree
|
||||
* (so `apps/studio/AGENTS.md` applies to `apps/studio/**`). Claude Code does NOT
|
||||
* read AGENTS.md, so this adds zero context cost there — Claude keeps using the
|
||||
* skills directly.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/sync-studio-guidelines.mjs # write apps/studio/AGENTS.md
|
||||
* node scripts/sync-studio-guidelines.mjs --check # exit 1 if the file is stale
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
|
||||
// Ordered list of skills to inline. Keep this list self-contained: if a skill
|
||||
// here references another via "See `x` skill", include `x` too so the reference
|
||||
// resolves to a section within this document.
|
||||
const SKILLS = [
|
||||
'studio-best-practices',
|
||||
'studio-ui-patterns',
|
||||
'vercel-composition-patterns',
|
||||
'studio-queries',
|
||||
'studio-error-handling',
|
||||
]
|
||||
|
||||
const OUTPUT_PATH = join(repoRoot, 'apps/studio/AGENTS.md')
|
||||
|
||||
/**
|
||||
* Strip YAML frontmatter (a leading `---` ... `---` block) and trim.
|
||||
* @param {string} source
|
||||
* @returns {string}
|
||||
*/
|
||||
function stripFrontmatter(source) {
|
||||
if (!source.startsWith('---')) return source.trim()
|
||||
const end = source.indexOf('\n---', 3)
|
||||
if (end === -1) return source.trim()
|
||||
const after = source.indexOf('\n', end + 1)
|
||||
return source.slice(after + 1).trim()
|
||||
}
|
||||
|
||||
function build() {
|
||||
const sections = SKILLS.map((name) => {
|
||||
const relPath = `.claude/skills/${name}/SKILL.md`
|
||||
const body = stripFrontmatter(readFileSync(join(repoRoot, relPath), 'utf8'))
|
||||
return `<!-- source: ${relPath} -->\n\n${body}`
|
||||
})
|
||||
|
||||
const header = [
|
||||
'<!--',
|
||||
' GENERATED FILE — DO NOT EDIT.',
|
||||
'',
|
||||
' Source of truth: the Studio skills in .claude/skills/ (listed below).',
|
||||
' Regenerate with: pnpm sync:guidelines',
|
||||
' CI fails if this file drifts from the skills (studio-guidelines-sync workflow).',
|
||||
'',
|
||||
' This file exists so CodeRabbit applies the Studio conventions when reviewing',
|
||||
' apps/studio code. CodeRabbit auto-detects **/AGENTS.md and scopes it to this',
|
||||
' directory tree. Claude Code does not read AGENTS.md and uses the skills directly.',
|
||||
'-->',
|
||||
'',
|
||||
'# Studio Code Review Guidelines',
|
||||
'',
|
||||
'Conventions for `apps/studio` code, compiled from the Studio skills. Apply these',
|
||||
'when reviewing changes under `apps/studio/`.',
|
||||
].join('\n')
|
||||
|
||||
return `${header}\n\n${sections.join('\n\n---\n\n')}\n`
|
||||
}
|
||||
|
||||
const isCheck = process.argv.includes('--check')
|
||||
const expected = build()
|
||||
|
||||
if (isCheck) {
|
||||
let actual = ''
|
||||
try {
|
||||
actual = readFileSync(OUTPUT_PATH, 'utf8')
|
||||
} catch {
|
||||
// missing file → stale
|
||||
}
|
||||
if (actual !== expected) {
|
||||
console.error(
|
||||
'apps/studio/AGENTS.md is out of date with the Studio skills.\n' +
|
||||
'Run `pnpm sync:guidelines` and commit the result.'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('apps/studio/AGENTS.md is up to date.')
|
||||
} else {
|
||||
writeFileSync(OUTPUT_PATH, expected)
|
||||
console.log(`Wrote apps/studio/AGENTS.md from ${SKILLS.length} skills.`)
|
||||
}
|
||||
Reference in new issue
Block a user