Files
supabase/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.utils.ts
T
CharisandJoshen Lim 8bdfe03fe7 refactor(studio): drop notebook type widening now that the API supports it (#49272)
## Summary

- Regenerates `packages/api-types` for the content endpoints now that
the Platform API's `notebook` content type has landed (list/get/upsert
`type` enums, plus `UpsertContentBody`'s notebook cell shape with
`_id`/`y_series`). Unrelated schema drift from the same regen
(Warehouse, SSO, notification exceptions, etc.) is excluded — only the
content-endpoint hunks are applied.
- Removes every local widening cast added while the API support was
pending (`content-query.ts`, `content-infinite-query.ts`,
`notebook-query.ts`, `notebook-upsert-mutation.ts`,
`sql-folders-query.ts`).
- What remains is scoped and renamed to match: draft ids
(`generateDraftId`/`isDraftId`), used only for cells created client-side
in the editor before their first save, dropped before they'd ever reach
the backend as a fake `_id`.

## Test plan

- [x] `pnpm typecheck` — clean
- [x] `pnpm --filter studio test` — full suite passes (518 files / 5471
tests)
- [x] `pnpm --filter studio run lint:ratchet` — no new warnings
- [x] `pnpm format` / prettier — clean

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved notebook cell tracking during editing, reordering, insertion,
and deletion.
* Preserved existing cell identifiers while removing temporary draft
identifiers before saving.
* Improved chart configuration for selecting and displaying multiple
Y-axis series.
  * Strengthened notebook validation and content persistence behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-08-20 13:06:41 +08:00

154 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import dayjs from 'dayjs'
import type { CodeBlockLang } from 'ui-patterns/CodeBlock'
import type {
NotebookCellDiffEntry,
OperationResultCell,
} from '@/data/content/notebooks/notebook-operations'
import type { TimeRange } from '@/data/content/notebooks/notebook-schema'
/** React key for a diff entry. Added/replaced cells have no `_id`, so they key off the operation. */
export function getEntryKey(entry: NotebookCellDiffEntry): string {
switch (entry._tag) {
case 'unchanged':
case 'removed':
case 'moved':
return entry.cell._id
case 'added':
case 'replaced':
return `op-${entry.operationIndex}`
}
}
/**
* Whether a diff entry starts expanded. Only the entries the user has to actually read to
* decide — the ones whose content the assistant is proposing — open on their own; unchanged,
* moved, and removed cells stay as single rows until asked for.
*/
export function isEntryExpandedByDefault(entry: NotebookCellDiffEntry): boolean {
return entry._tag === 'added' || entry._tag === 'replaced'
}
/** Human label for a collapsed/badge row. */
export function getCellLabel(cell: OperationResultCell): string {
switch (cell._tag) {
case 'markdown_cell':
return 'Markdown cell'
case 'database_cell':
case 'log_cell':
return `Query: ${cell.title ?? 'Untitled query'}`
}
}
/** The cell's underlying source text, regardless of backend. */
export function getCellSourceText(cell: OperationResultCell): string {
switch (cell._tag) {
case 'markdown_cell':
return cell.text
case 'database_cell':
case 'log_cell':
return cell.sql
}
}
/** Language for rendering the cell's source via `CodeBlock`. */
export function getCellCodeBlockLanguage(cell: OperationResultCell): CodeBlockLang {
switch (cell._tag) {
case 'markdown_cell':
return 'markdown'
case 'database_cell':
case 'log_cell':
return 'sql'
}
}
/** Monaco language id for rendering the cell's source via `DiffEditor`. */
export function getCellMonacoLanguage(cell: OperationResultCell): string {
switch (cell._tag) {
case 'markdown_cell':
return 'markdown'
case 'database_cell':
case 'log_cell':
return 'pgsql'
}
}
/** Formats a `TimeRange` as plain text, e.g. "Last 7 days" or an absolute bound pair. */
export function formatTimeRange(range: TimeRange): string {
if (range._tag === 'relative_time_range') {
return `Last ${range.amount} ${range.unit}${range.amount === 1 ? '' : 's'}`
}
const format = (value: string) => dayjs(value).format('MMM D, YYYY h:mm A')
return `${format(range.start)} → ${format(range.end)}`
}
/**
* Plain-text metadata line for a query cell (its source parameters, not its SQL) — `null`
* when the cell has none. A `replace_cell` can change only this and leave `sql` identical,
* so it's compared independently of the source text rather than folded into it.
*/
export function getCellMetadataLine(cell: OperationResultCell): string | null {
switch (cell._tag) {
case 'markdown_cell':
return null
case 'database_cell':
return cell.database_identifier ? `Database: ${cell.database_identifier}` : null
case 'log_cell':
return `Time range: ${formatTimeRange(cell.time_range)}`
}
}
export type NotebookDiffSummary =
| { mode: 'create'; cellCount: number }
| { mode: 'update'; counts: { added: number; removed: number; replaced: number; moved: number } }
/** Summarizes a set of diff entries into counts suitable for a header line. */
export function summarizeNotebookDiff(
entries: NotebookCellDiffEntry[],
mode: 'create' | 'update'
): NotebookDiffSummary {
if (mode === 'create') {
return { mode: 'create', cellCount: entries.length }
}
const counts = { added: 0, removed: 0, replaced: 0, moved: 0 }
for (const entry of entries) {
switch (entry._tag) {
case 'added':
counts.added++
break
case 'removed':
counts.removed++
break
case 'replaced':
counts.replaced++
break
case 'moved':
counts.moved++
break
case 'unchanged':
break
}
}
return { mode: 'update', counts }
}
/** Formats a `NotebookDiffSummary` into the header string. */
export function formatNotebookDiffSummary(summary: NotebookDiffSummary): string {
if (summary.mode === 'create') {
const { cellCount } = summary
return `${cellCount} cell${cellCount === 1 ? '' : 's'}`
}
const { added, removed, replaced, moved } = summary.counts
const parts: string[] = []
if (added > 0) parts.push(`+${added}`)
if (removed > 0) parts.push(`−${removed}`)
if (replaced > 0) parts.push(`~${replaced}`)
if (moved > 0) parts.push(`↕${moved}`)
return parts.length > 0 ? parts.join(' ') : 'No changes'
}