Skip to main content

data-app-actions

Make a Metabase data app write data — a table action that inserts, updates or deletes a row of a published table, defined with `defineAction` in `src/metabase.actions.ts`, and the `useAction` hook from `@metabase/embedding-sdk-react/data-app` behind a form or a button, with inputs typed from each parameter, the server's error shown as it comes, and every affected query refreshed. Triggers — "a form that saves a row", "a button that updates a record", "delete an entry from the app", "submit doesn't refresh the list", "the action fails in the app". Reads are `data-app-data`.

معلومات المصدر

المستودع
metabase/metabase-cli
آخر نشاط في المصدر
٣١ أكتوبر ٢٠٢٦ في ٠١:٠٢
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
١٧
التفرعات
٧

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
data-app-actions
description
Make a Metabase data app write data — a table action that inserts, updates or deletes a row of a published table, defined with `defineAction` in `src/metabase.actions.ts`, and the `useAction` hook from `@metabase/embedding-sdk-react/data-app` behind a form or a button, with inputs typed from each parameter, the server's error shown as it comes, and every affected query refreshed. Triggers — "a form that saves a row", "a button that updates a record", "delete an entry from the app", "submit doesn't refresh the list", "the action fails in the app". Reads are `data-app-data`.
allowed-tools
Read, Write, Edit, Bash, AskUserQuestion
requires
["dataApps"]
# Writes in a data app A data app writes through **table actions**: an insert, update or delete of one row of a table published to the Library, by its primary key. It defines each one in `src/metabase.actions.ts`, one export per action: ```ts import { defineAction } from "@metabase/embedding-sdk-react/data-app"; import { schema } from "./metabase.schema"; export const updateOrder = defineAction({ entityId: "J2mXq8sLb0vT5nYcW4kRd", kind: "row/update", table: schema.tables.orders, }); ``` - `kind` is `row/create`, `row/update` or `row/delete`; `table` is a table of `schema` (`data-app-data`). The table needs exactly one primary key, and its database needs data actions turned on (_Data actions on the database_): settle both before writing the export. Show the user the table and kind and add it once they agree; a request that already names the write ("let me add a note") is that agreement. - `entityId` comes from `mb nano-id`, never invented; keep it when editing the export. The export's name is the action's name in Metabase. - There are no query actions and no SQL: a write a table action can't do (a computed value, several rows, a row matched by another column) is out of reach for the app — say so. Never copy an action from a data actions collection. - To drop an action, delete its export. The dev preview runs the action without saving it; Metabase runs it once the app is released (`data-app`, _Release_). ## Data actions on the database Before the first action on a database, read its toggle: `mb db get <db-id> --full --json --profile <name> | jq '.settings["database-enable-actions"]'` (`<db-id>` from `mb db list --profile <name> --json`, or `mb table get <table-id> --json --profile <name> | jq .db_id` once `src/metabase.schema.ts` gives the table's `id`). Anything but `true`, an unset `null` included, means it's off, and every action on that database fails with `Actions are not enabled.`, in the preview and in Metabase. Then ask with `AskUserQuestion` ("Data actions are off on <database>. Turning them on lets anyone with access to its actions write to it."), offering: - **Turn them on now**: run `mb db set-data-actions <db-id> true --profile <name>`, then read the toggle again. - **I'll turn them on**: the user does it in Admin → Databases → the database ("Allow data actions that use this database to be run"); wait for them, then read the toggle again before going on. - **No writes**: build the app without actions, and tell the user which features that leaves out. Never turn the toggle on without that yes. ## The hook ```tsx import { useAction } from "@metabase/embedding-sdk-react/data-app"; import { updateOrder } from "../metabase.actions"; const { execute, isExecuting, result, error, reset } = useAction(updateOrder); ``` - **Import it from `/data-app`.** The main `@metabase/embedding-sdk-react` entry exports the general SDK `useAction`, which takes raw ids and isn't typed from the definition. Write no generics: the definition types `execute`'s keys and values. - `execute(params)` resolves to the response body; on failure it both throws and sets `error`. It resolves to `null` without sending anything while the SDK is still starting, so guard a call that can happen at mount. It never runs on its own and has no `enabled`: return early in the handler to skip it. - `isExecuting` is true until it settles: disable the trigger and show progress, so a double click can't send twice. - `result` is the last response, `null` before the first call: `{ "created-row": <the new row> }` for an insert, `{ "rows-updated": <n> }` for an update, `{ "rows-deleted": <n> }` for a delete. `reset()` clears `result` and `error`, for example when the user dismisses a message. ## A form ```tsx export function NoteForm({ orderId, onSaved }: { orderId: number; onSaved: () => Promise<void> }) { const { execute, isExecuting, error } = useAction(updateOrder); const [note, setNote] = useState(""); const [isValid, setIsValid] = useState(false); async function onSubmit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); try { await execute({ id: orderId, note }); // keys are column names, in lowercase await onSaved(); // the parent refetches what the write changed setNote(""); setIsValid(false); } catch { // shown from `error` } } return ( <form onSubmit={onSubmit} onChange={(event) => setIsValid(event.currentTarget.checkValidity())}> <input type="text" required value={note} onChange={(event) => setNote(event.target.value)} /> <button type="submit" disabled={isExecuting || !isValid}> {isExecuting ? "Saving…" : "Save"} </button> {error ? ( <pre style={{ whiteSpace: "pre-wrap", margin: 0 }}> {error.data.message ?? "Action failed."} </pre> ) : null} </form> ); } ``` **Parameters.** The keys `execute` takes are the table's column names in lowercase (`CREATED_AT` becomes `created_at`; names already in lowercase stay as they are), each typed from its field's `jsType` in `src/metabase.schema.ts`, and all optional in the type. Pick each input from `jsType`, never from the name — a column called `email` with `jsType: "string"` is still `type="text"`. | `jsType` | Input | | --------- | ---------------------------------------------------------- | | `string` | `type="text"`; a `<textarea>` for long text such as a note | | `number` | `type="number"`, read with `valueAsNumber` | | `boolean` | a checkbox or switch | | `Date` | `type="date"`, or `datetime-local` when a time is expected | | `unknown` | `type="text"`, converted at the call | - **Insert** (`row/create`): every column but an auto-increment key and the columns the database fills when the form leaves them out, such as a `created_at` or `status` with a default. `mb table get <id> --include fields --full --json` gives each field's `database_required` and `database_default`. `result["created-row"]` holds the new row, its key among them. - **Update** (`row/update`): the key and the columns to change. An edit form starts from the row's current values, read by the query on screen, and sends them all; a button that sets one column (mark as done) sends only the key and that column. A fixed value such as `"done"` comes from the column's values (`mb field values <field-id> --profile <name>`), never a guess. A `null` is dropped, so an update can't clear a column. - **Delete** (`row/delete`): the key only. Ask the viewer to confirm first. **Validation** is `required` on the key of an update or delete and on each `NOT NULL` column without a default in an insert, and nothing else — no length, range or format checks of your own. Disable submit from the browser's verdict, `form.checkValidity()`, never from hand-rolled checks. Anything stricter comes back from the server. **Errors.** `error` is `{ status?, data: { message?, errors? }, isCancelled }` — read it without a cast. Refusals and constraint errors are 400s whose `data.message` carries the server's text (`Sorry, the row you're trying to update doesn't exist`): show it as it comes, in a `white-space: pre-wrap` element, since driver messages span lines — never "Something went wrong", `String(error)` or a friendlier paraphrase. `error.data.errors?.[parameter]`, when present, goes beside its field (`aria-invalid` on the input). ## Refresh After a write succeeds, refetch every query on screen it could have changed: `await Promise.all([orders.refetch(), totals.refetch()])`, the `refetch` that `useMetabaseQuery` returns. - Those queries live in a component above the trigger, which passes the refresh down; a query in the trigger's own component or below it can't be wired to it. - A `StaticQuestion` panel has no `refetch`: give it a `key` the parent bumps after the write, which remounts it and runs the question again. - Await the refresh before clearing the form or closing the modal, so stale rows never flash. - Never prepend `result` to a local list instead: ids, timestamps and computed columns come from the server. ## When it doesn't work 1. Log `result`, `error` and the object passed to `execute`: every key is a lowercase column name, every value matches its `jsType`. 2. `execute` resolved to `null` and nothing was sent: it ran before the SDK started. 3. The screen doesn't update: see _Refresh_. 4. It works in the preview but not in Metabase: the app was released without running `mb data-app generate-actions` (`data-app`, _Release_). Then release the app (`data-app`, _Release_).
عرض على GitHub