Files
supabase/apps/studio/components/interfaces/Reports/UpdateModal.tsx
Charis 957e9fec67 feat(studio): notebook content at the API boundary (#48815)
## Summary

Stacked on #48813 (1.2: notebook content schema). Part of
[FE-4109](https://linear.app/supabase/issue/FE-4109/notebooks-data-model)
— see that issue for the rest of the notebooks data-model stack.

- Teach `content-remap.ts`'s wire↔domain dispatcher about the `notebook`
content type, branding each cell's `sql` per `_tag` via the notebook
schemas added in 1.2 (parses through `notebookDomainSchema` on the way
in, unbrands per cell on the way out).
- Add `{ type: 'notebook'; content: Notebooks.Content }` to the
`Content` union in `content-query.ts`, plus a `ContentOfType<T>` helper
for narrowing it.
- Fix the resulting narrowing fallout at call sites that assumed
`Content` only ever meant `sql`/`report`/`log_sql`: two
generated-query-param casts, and four report/logs call sites now
narrowed via `ContentOfType<'report'>` / `ContentOfType<'log_sql'>`.

## Test plan

- [x] `pnpm --filter studio vitest run data/content/` — 35 tests pass,
including new notebook coverage in `content-remap.test.ts` (per-cell
brand separation, missing-field throw, remap↔unmap round-trip)
- [x] `pnpm typecheck` — clean (pre-existing unrelated `ui-patterns`
error aside)
- [x] `pnpm --filter studio lint` — no new warnings/errors on changed
files
- [x] `pnpm format` — clean

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-08-10 10:15:24 -04:00

147 lines
4.2 KiB
TypeScript

import { zodResolver } from '@hookform/resolvers/zod'
import { useParams } from 'common'
import { useEffect } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
Form,
FormControl,
FormField,
Input,
Textarea,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import * as z from 'zod'
import { ContentOfType } from '@/data/content/content-query'
import { useContentUpsertMutation } from '@/data/content/content-upsert-mutation'
const formSchema = z.object({
name: z.string().min(1, 'Required'),
description: z.string().optional(),
})
type CustomReport = z.infer<typeof formSchema>
export interface UpdateCustomReportProps {
selectedReport?: ContentOfType<'report'>
initialValues: CustomReport
onCancel: () => void
}
export const UpdateCustomReportModal = ({
selectedReport,
initialValues,
onCancel,
}: UpdateCustomReportProps) => {
const { ref } = useParams()
const { mutate: updateReport, isPending: isUpdating } = useContentUpsertMutation({
onSuccess: () => {
toast.success('Successfully updated report')
onCancel()
},
onError: (error) => {
toast.error(`Failed to update report: ${error.message}`)
},
})
const onConfirmUpdateReport: SubmitHandler<CustomReport> = (newVals) => {
if (!ref) return console.error('Project ref is required')
if (!selectedReport) return
if (!selectedReport.id) return
if (!selectedReport.project_id) return
updateReport({
projectRef: ref,
payload: {
...selectedReport,
owner_id: selectedReport.owner_id!,
project_id: selectedReport.project_id,
id: selectedReport.id,
name: newVals.name,
description: newVals.description || '',
},
})
}
const handleCancel = () => {
onCancel()
form.reset()
}
const form = useForm<CustomReport>({
resolver: zodResolver(formSchema),
defaultValues: initialValues,
})
const { formState, reset } = form
const { isDirty } = formState
useEffect(() => {
if (isDirty) return
reset(initialValues)
}, [initialValues, isDirty, reset])
return (
<Dialog open={selectedReport !== undefined} onOpenChange={handleCancel}>
<DialogContent size="small">
<DialogHeader>
<DialogTitle>Update custom report</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
<Form {...form}>
<form onSubmit={form.handleSubmit(onConfirmUpdateReport)} noValidate>
<DialogSection>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItemLayout name="name" layout="vertical" label="Name">
<FormControl>
<Input {...field} id="name" />
</FormControl>
</FormItemLayout>
)}
/>
</DialogSection>
<DialogSection>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItemLayout name="description" layout="vertical" label="Description">
<FormControl>
<Textarea
{...field}
id="description"
rows={4}
placeholder="Describe your custom report"
className="resize-none"
/>
</FormControl>
</FormItemLayout>
)}
/>
</DialogSection>
<DialogFooter>
<Button type="reset" variant="default" onClick={handleCancel} disabled={isUpdating}>
Cancel
</Button>
<Button type="submit" loading={isUpdating} disabled={isUpdating || !isDirty}>
Save custom report
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
)
}