metabase-data-app-actions

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Triggering actions from a Metabase data app

在Metabase数据应用中触发动作

A Metabase action is a server-defined write operation against the data warehouse — either a basic CRUD operation (insert / update / delete) on a model, or a custom SQL command. Actions are configured ahead of time on the Metabase instance, with their parameters, model bindings, and permissions already set. A data app's job is to invoke an action with the right parameters when the user does something — clicking a button, submitting a form, confirming a destructive prompt.
Metabase 动作(action) 是在服务器端定义的、针对数据仓库的写入操作——可以是针对模型的基础CRUD操作(新增/更新/删除),也可以是自定义SQL命令。动作需要提前在Metabase实例中配置完成,其参数、模型绑定关系和权限都已预先设置。数据应用的职责是在用户执行操作时(点击按钮、提交表单、确认危险操作提示等),调用对应动作并传入正确的参数。

The mental model

心智模型

Actions belong to a model. They mutate that model's rows. Concretely:
  • Every action has a parent model. In the schema it appears as
    schema.models.<modelName>.actions.<actionName>
    .
  • Model entries are included here only to catalog actions. Do not render models as questions, pass model ids to
    InteractiveQuestion
    , or fetch model rows; use semantic-layer queries/questions for read views.
  • An action's
    type
    is either
    "implicit"
    (CRUD on the model) or
    "query"
    (custom SQL the user authored).
  • Implicit actions have an
    implicitKind
    that says what they do:
    "row/create"
    ,
    "row/update"
    ,
    "row/delete"
    , or
    "bulk/*"
    variants.
  • Each action publishes a
    parameters
    list. Each parameter has a
    slug
    (the key the Data App sends), a
    jsType
    (
    "string"
    /
    "number"
    /
    "Date"
    /
    "boolean"
    /
    "unknown"
    ), and an optional
    required
    flag.
  • Use
    action.parameters
    to know which fields to render and submit. A create result may include
    result["created-row"]
    , but that row is only typed as
    Record<string, RowValue>
    ; use it for lightweight confirmation, then refresh the existing table/question/query data already used by the page. Do not fetch or render the parent model itself.
动作隶属于某个模型,用于修改该模型的行数据。具体来说:
  • 每个动作都有一个父模型。在schema中,它的路径为
    schema.models.<modelName>.actions.<actionName>
    。
  • 此处包含模型条目仅用于对动作进行分类。不要将模型渲染为问题(question)、不要将模型ID传递给
    InteractiveQuestion
    ,也不要直接获取模型行数据;读取视图请使用语义层查询/问题。
  • 动作的
    type
    分为
    "implicit"
    (模型的CRUD操作)和
    "query"
    (用户编写的自定义SQL)两种。
  • 隐式动作(implicit action)有一个
    implicitKind
    字段用于说明操作类型:
    "row/create"
    、
    "row/update"
    、
    "row/delete"
    ,以及
    "bulk/*"
    类的批量操作变体。
  • 每个动作都会对外暴露一个
    parameters
    列表。每个参数包含
    slug
    (数据应用发送时使用的键名)、
    jsType
    (
    "string"
    /
    "number"
    /
    "Date"
    /
    "boolean"
    /
    "unknown"
    ),以及可选的
    required
    标识。
  • 请通过
    action.parameters
    来确定需要渲染和提交哪些字段。创建操作的结果可能包含
    result["created-row"]
    ,但该行的类型仅为
    Record<string, RowValue>
    ;可将其用于轻量级的确认提示,随后刷新页面已在使用的表格/问题/查询数据。不要直接获取或渲染父模型本身。

What's in the schema (and what isn't)

schema包含(及不包含)的内容

Action entries are only generated when the typed schema includes models. Before writing action-invoking code, make sure the schema was generated with
include-models=true
; with
database=<name-or-id>&include-models=true
, Metabase includes models/actions for that database only. Do not rely on
question-collections
for actions; question collections only add saved questions.
Before writing any action-invoking code, look at the schema and enumerate what's available under
schema.models.<m>.actions
across the models the app cares about. The schema is your complete catalog of actions for the instance, not a catalog of model data to display. If a model's
actions
entry has
create
,
update
,
delete
, those are the actions invokable. Anything not present doesn't exist as far as the Data App is concerned.
只有当类型化schema包含模型时,才会生成动作条目。在编写动作调用代码之前,请确保schema是通过
include-models=true
参数生成的;如果使用
database=<name-or-id>&include-models=true
,Metabase只会包含该数据库的模型/动作。不要依赖
question-collections
来获取动作;问题集合仅包含已保存的问题。
在编写任何动作调用代码之前,请查看schema,枚举应用所关心的所有模型下
schema.models.<m>.actions
中可用的动作。schema是实例中动作的完整目录,而不是用于展示的模型数据目录。如果某个模型的
actions
条目包含
create
、
update
、
delete
,这些就是可调用的动作。对于数据应用来说,任何未出现在schema中的动作都视为不存在。

The hook

Hook介绍

ts
import { useAction } from "@metabase/embedding-sdk-react/data-app";

const { execute, isExecuting, result, error, reset } = useAction(MyAction);
  • Import it from
    @metabase/embedding-sdk-react/data-app
    .
    That entry's
    useAction
    is the data-app form: it accepts a
    defineAction(...)
    export or
    null
    and nothing else. The main entry's
    useAction
    is the general SDK hook, which also takes plain objects and raw ids, so it cannot enforce the definition; a data app that imports it from the main entry compiles with an unsynchronized action and fails in production.
  • The argument is the
    defineAction(...)
    export itself, declared in the app's root-level
    actions/
    directory (one
    <topic>.action.ts
    file per topic, beside
    package.json
    , never under
    src/
    ) — pass
    MyAction
    , not
    MyAction.copiedActionId
    . The hook accepts nothing else in a data app: an inline
    { action: ... }
    object, the schema entry, or a spread copy of a definition fails to compile with
    Property 'definedWithDefineAction' is missing
    . Fix that by adding the export to
    actions/
    , not with a cast and not by calling
    defineAction(...)
    at the hook, which compiles but is never synchronized. A production build then runs the synchronized copy, whose model sits in the app's own collection; the dev preview keeps running the authored action, so an app works before it has ever been synchronized. Do not pass
    schema.models.<model>.actions.<action>
    or its
    .id
    : the entry is a compile error, and the raw id belongs to the original model, which the app's users cannot read, so the call fails on permissions in production.
  • Don't write the generics. The definition carries its schema entry, so the hook infers both the parameters object and the discriminated
    result
    from it:
    parameters[]
    becomes a keyed object (
    required: true
    entries are required keys, each value typed from its
    jsType
    ), and
    implicitKind
    /
    type
    become the kind (
    "row/create"
    →
    "create"
    ,
    "row/update"
    →
    "update"
    ,
    "row/delete"
    →
    "delete"
    , any
    "bulk/*"
    →
    "bulk"
    ,
    type === "query"
    →
    "sql"
    ). The raw-id form exists only on the SDK's
    useAction
    from the main entry, where
    useAction<TParameters, TKind>(42)
    needs them spelled out since an id describes nothing; a data app never names an action by id.
  • execute(parameters)
    — triggers the action. Parameters object is keyed by parameter
    slug
    ; parameters declared
    required: true
    are required keys, everything else optional. Returns the response body on success AND throws on failure (the error is also written to
    error
    state for render-time consumers). Resolves to
    null
    (without making a request) when
    actionId
    is
    null
    or the SDK is not yet initialized — guard the call site if those cases are reachable.
  • No
    enabled
    /
    options
    argument.
    The hook only ever runs when
    execute(...)
    is called, so a gate option would be redundant. Skip the action by branching in the event handler:
    ts
    const onClick = async () => {
      if (!user.canEdit) return;
      await execute({ id: orderId, discount });
    };
  • isExecuting
    —
    true
    between the call and its resolution. Drive button
    disabled
    from this so the user can't double-click into duplicate requests.
  • result
    — the response body, discriminated by
    TKind
    (or the
    AnyActionResult
    union when
    TKind
    is omitted).
    null
    before the first call and after
    reset()
    . Use it for lightweight confirmation (
    result?.["created-row"]
    after an insert,
    result?.["rows-affected"]
    after a SQL action), but do not treat create rows as richly typed model rows; refresh surrounding data instead — see After an action runs.
  • error
    — the last thrown error, typed
    ActionExecuteError | null
    . Read fields directly with no cast:
    error?.data?.message
    ,
    error?.status
    ,
    error?.isCancelled
    .
  • reset()
    — clears
    result
    and
    error
    back to
    null
    . Useful after the user acknowledges success or dismisses an error.
The hook does NOT auto-fire on mount. Actions only run when the Data App calls
execute(...)
explicitly.
ts
import { useAction } from "@metabase/embedding-sdk-react/data-app";

const { execute, isExecuting, result, error, reset } = useAction(MyAction);
  • 请从
    @metabase/embedding-sdk-react/data-app
    导入。
    该入口导出的
    useAction
    是数据应用专用版本:它仅接受
    defineAction(...)
    的导出结果或
    null
    ,不接受其他参数。主入口的
    useAction
    是通用SDK hook,还可以接受普通对象和原始ID,因此无法对动作定义进行校验;从主入口导入
    useAction
    的数据应用在编译时会使用未同步的动作,在生产环境中会运行失败。
  • 入参是
    defineAction(...)
    的导出结果本身,该定义需声明在应用根目录的
    actions/
    文件夹下(每个主题对应一个
    <topic>.action.ts
    文件,放在
    package.json
    同级目录,绝不能放在
    src/
    下)——请传递
    MyAction
    ,而不是
    MyAction.copiedActionId
    。在数据应用中,该hook不接受其他任何形式的参数:内联的
    { action: ... }
    对象、schema条目、或者定义的展开拷贝都会编译失败,报错
    Property 'definedWithDefineAction' is missing
    。请通过将导出添加到
    actions/
    目录来解决此问题,不要使用类型断言,也不要在hook调用处直接调用
    defineAction(...)
    ——后者虽然能通过编译,但永远不会被同步。生产构建会运行同步后的动作副本,其模型位于应用自己的集合中;而开发预览会继续运行原始编写的动作,因此应用在同步之前就可以正常工作。切勿传递
    schema.models.<model>.actions.<action>
    或它的
    .id
    :前者会导致编译错误,而原始ID属于原模型,应用的用户没有读取权限,因此在生产环境中调用会因权限不足失败。
  • 不要手动编写泛型。 动作定义本身携带了对应的schema条目,因此hook可以自动推断参数对象和可区分的
    result
    类型:
    parameters[]
    会转换为带键的对象(
    required: true
    的条目为必填键,每个值的类型由其
    jsType
    决定),
    implicitKind
    /
    type
    会转换为操作类型(
    "row/create"
    →
    "create"
    、
    "row/update"
    →
    "update"
    、
    "row/delete"
    →
    "delete"
    、任何
    "bulk/*"
    →
    "bulk"
    、
    type === "query"
    →
    "sql"
    )。原始ID形式的调用仅存在于SDK主入口的
    useAction
    中,在那里
    useAction<TParameters, TKind>(42)
    需要手动指定泛型,因为ID本身不包含任何类型信息;数据应用永远不会通过ID来引用动作。
  • execute(parameters)
    —— 触发动作。参数对象以参数的
    slug
    作为键名;声明为
    required: true
    的参数是必填键,其余为可选。调用成功时返回响应体,失败时会抛出错误(错误同时会被写入
    error
    状态,供渲染时使用)。当
    actionId
    为
    null
    或SDK尚未初始化时,该方法会解析为
    null
    (不会发起请求)——如果可能出现这些情况,请在调用处做好防护。
  • 没有
    enabled
    /
    options
    参数。
    该hook仅在调用
    execute(...)
    时才会运行,因此开关类参数是多余的。请在事件处理函数中通过条件判断来跳过动作执行:
    ts
    const onClick = async () => {
      if (!user.canEdit) return;
      await execute({ id: orderId, discount });
    };
  • isExecuting
    —— 在调用发起和结果返回之间为
    true
    。请用这个值来控制按钮的
    disabled
    状态,避免用户双击导致重复请求。
  • result
    —— 响应体,类型由
    TKind
    区分(如果省略
    TKind
    则为
    AnyActionResult
    联合类型)。首次调用前和调用
    reset()
    后值为
    null
    。可将其用于轻量级确认提示(比如插入操作后读取
    result?.["created-row"]
    ,SQL动作后读取
    result?.["rows-affected"]
    ),但不要将创建的行视为完整类型的模型行;请刷新周边的数据——参见动作执行后。
  • error
    —— 最近一次抛出的错误,类型为
    ActionExecuteError | null
    。可直接读取其字段,无需类型断言:
    error?.data?.message
    、
    error?.status
    、
    error?.isCancelled
    。
  • reset()
    —— 将
    result
    和
    error
    清空为
    null
    。在用户确认成功或关闭错误提示后非常有用。
该Hook不会在组件挂载时自动执行。 只有当数据应用显式调用
execute(...)
时,动作才会运行。

Canonical usage — a form that creates a row

典型用法——创建行的表单

tsx
import { useAction } from "@metabase/embedding-sdk-react/data-app";

import { CreatePerson } from "../../actions/people.action";

function AddPersonForm({ onCreated }: { onCreated: () => void }) {
  const { useState } = React;
  const { execute, isExecuting, error, reset } = useAction(CreatePerson);

  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  async function onSubmit(e: React.FormEvent) {
    e.preventDefault();
    try {
      await execute({ name, email });   // typed: keys match parameter slugs
      setName("");
      setEmail("");
      onCreated();                      // ← let the parent refresh dependent data
    } catch {
      // error is captured into hook state for render-time display
    }
  }

  return (
    <form onSubmit={onSubmit}>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <button type="submit" disabled={isExecuting || !name || !email}>
        {isExecuting ? "Saving…" : "Add"}
      </button>
    </form>
  );
}
tsx
import { useAction } from "@metabase/embedding-sdk-react/data-app";

import { CreatePerson } from "../../actions/people.action";

function AddPersonForm({ onCreated }: { onCreated: () => void }) {
  const { useState } = React;
  const { execute, isExecuting, error, reset } = useAction(CreatePerson);

  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  async function onSubmit(e: React.FormEvent) {
    e.preventDefault();
    try {
      await execute({ name, email });   // typed: keys match parameter slugs
      setName("");
      setEmail("");
      onCreated();                      // ← let the parent refresh dependent data
    } catch {
      // error is captured into hook state for render-time display
    }
  }

  return (
    <form onSubmit={onSubmit}>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <button type="submit" disabled={isExecuting || !name || !email}>
        {isExecuting ? "Saving…" : "Add"}
      </button>
    </form>
  );
}

Showing the error message

显示错误信息

When
execute(...)
fails, surface both error layers. The hook's
error
is typed
ActionExecuteError | null
— shape
{ status?, data: { message?, errors? }, isCancelled }
.
error.data.message
is the whole-request failure.
error.data.errors
is a per-field validation map keyed by parameter slug (
{ <slug>: <message> }
), or
{}
for whole-request failures. Read both directly, no cast:
tsx
const fieldErrors = error?.data.errors ?? {};

{error ? (
  <pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
    {error.data.message ?? "Action failed."}
  </pre>
) : null}

{action.parameters.map((parameter) => {
  const fieldError = fieldErrors[parameter.slug];
  return (
    <label key={parameter.slug}>
      {parameter.displayName}
      <input
        aria-invalid={Boolean(fieldError)}
        style={{ borderColor: fieldError ? "#dc2626" : undefined }}
      />
      {fieldError ? <div role="alert">{fieldError}</div> : null}
    </label>
  );
})}
Use
<pre>
(or any element with
white-space: pre-wrap
) — the messages contain newlines that matter (driver errors include the SQL on its own line). A
<span>
collapses them into one wall of text.
Example: submitting a 9-character value into a
CHARACTER(2)
column produces
Value too long for column "STATE CHARACTER(2)": "'dadasdasd' (9)";
SQL statement:
UPDATE "PUBLIC"."PEOPLE" SET … WHERE "PUBLIC"."PEOPLE"."ID" = 1 [22001-214]
That whole string is
error.data.message
. Render it as-is.
Don't:
  • Render
    "Failed"
    /
    "Something went wrong"
    /
    String(error)
    instead of
    error.data.message
    — the user loses the only fix-it info they have.
  • Render only
    error.data.message
    for validation failures —
    error.data.errors[parameter.slug]
    is often the actionable "which field and why" detail.
  • Paraphrase the message into a "friendlier" version. The raw H2/Postgres/SQL error is more actionable than any rewrite.
当
execute(...)
失败时,请同时展示两层错误信息。该hook的
error
类型为
ActionExecuteError | null
——结构为
{ status?, data: { message?, errors? }, isCancelled }
。
error.data.message
是整个请求的错误信息。
error.data.errors
是按参数
slug
为键的字段级校验错误映射(
{ <slug>: <message> }
),如果是全局请求错误则为
{}
。请直接读取这两个字段,无需类型断言:
tsx
const fieldErrors = error?.data.errors ?? {};

{error ? (
  <pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
    {error.data.message ?? "Action failed."}
  </pre>
) : null}

{action.parameters.map((parameter) => {
  const fieldError = fieldErrors[parameter.slug];
  return (
    <label key={parameter.slug}>
      {parameter.displayName}
      <input
        aria-invalid={Boolean(fieldError)}
        style={{ borderColor: fieldError ? "#dc2626" : undefined }}
      />
      {fieldError ? <div role="alert">{fieldError}</div> : null}
    </label>
  );
})}
请使用
<pre>
(或任何带有
white-space: pre-wrap
样式的元素)——错误信息中包含重要的换行符(驱动错误会将SQL单独占一行)。使用
<span>
会将换行折叠成一整段文本,难以阅读。
示例:向
CHARACTER(2)
类型的列提交一个9字符的值会产生如下错误:
Value too long for column "STATE CHARACTER(2)": "'dadasdasd' (9)";
SQL statement:
UPDATE "PUBLIC"."PEOPLE" SET … WHERE "PUBLIC"."PEOPLE"."ID" = 1 [22001-214]
这整段字符串就是
error.data.message
的内容,请原样渲染。
不要做以下操作:
  • 不要渲染
    "Failed"
    /
    "Something went wrong"
    /
    String(error)
    来代替
    error.data.message
    ——用户会因此失去唯一能帮助他们解决问题的信息。
  • 校验失败时不要只渲染
    error.data.message
    ——
    error.data.errors[parameter.slug]
    通常包含更具可操作性的「哪个字段、为什么出错」的细节。
  • 不要将错误信息改写成「更友好」的版本。原始的H2/Postgres/SQL错误比任何改写都更具可操作性。

After an action runs — keeping the UI honest

动作执行后——保持UI数据准确

When an action succeeds, the data the user sees may be stale. Without an explicit refresh, the list still shows the old rows; the stat tile still shows the old count; the row the user just edited still shows its old values. The action worked, but the UI lies — and there is no warning.
The rule is simple and absolute: after an action resolves successfully, every piece of data on the screen that the action could have changed must be refreshed.
当动作执行成功后,用户看到的数据可能已经过时。如果没有显式刷新,列表仍然显示旧的行数据;统计卡片仍然显示旧的计数;用户刚刚编辑的行仍然显示旧的值。动作本身执行成功了,但UI展示的是错误信息——而且没有任何警告。
这条规则简单而绝对:动作成功执行后,屏幕上所有可能被该动作修改的数据都必须刷新。

Mapping parameters

参数映射

Each
parameters[]
entry on a schema action exposes
slug
,
displayName
,
jsType
, and optionally
required
. The Data App's job:
  • Object keys in
    execute({ … })
    match the parameter
    slug
    strings.
    Use them as literal string keys — the shape the hook derives from the definition rejects typos at compile time, but only when the argument is the
    defineAction(...)
    export (not a bare id).
  • Pick the right input
    type
    from
    jsType
    so the browser provides the correct UX and built-in coercion. The agent never invents the input type from the slug name (a slug called
    "phone"
    is still
    jsType: "string"
    →
    type="text"
    ).
    jsType
    Input element
    "string"
    <input type="text" …>
    "number"
    <input type="number" …>
    "boolean"
    <input type="checkbox" …>
    (or a Mantine
    Switch
    /
    Checkbox
    )
    "Date"
    <input type="date" …>
    (or
    "datetime-local"
    if a time component is expected)
    "unknown"
    <input type="text" …>
    — best effort, coerce at the call site
  • Value types in
    execute({ … })
    match
    jsType
    .
    A
    jsType: "number"
    parameter wants a
    number
    , not a string. With
    type="number"
    the input emits a number for
    .valueAsNumber
    , but
    <input>.value
    is still a string — coerce at the call site (
    Number(input)
    ) if you're reading the latter.
  • required: true
    parameters cannot be omitted.
    Reflected in the TS type: required keys are required.
  • displayName
    is for labels; never use it as a key.
For implicit actions, the slugs match the model's column names (slugified). For custom SQL actions, slugs are whatever the action's author named the SQL parameters in Metabase. Either way, the schema is the source of truth.
schema中动作的每个
parameters[]
条目都会暴露
slug
、
displayName
、
jsType
,以及可选的
required
。数据应用需要做的是:
  • execute({ … })
    中的对象键名必须与参数的
    slug
    字符串完全匹配。
    请将它们作为字面字符串键使用——只有当入参是
    defineAction(...)
    的导出结果(而不是裸ID)时,hook从定义中推导的类型才能在编译时捕获拼写错误。
  • 根据
    jsType
    选择正确的输入框
    type
    ,这样浏览器才能提供正确的用户体验和内置的类型转换。不要根据slug的名称来猜测输入类型(比如名为
    "phone"
    的slug,其
    jsType
    仍然是
    "string"
    → 对应
    type="text"
    )。
    jsType
    对应输入元素
    "string"
    <input type="text" …>
    "number"
    <input type="number" …>
    "boolean"
    <input type="checkbox" …>
    (或Mantine的
    Switch
    /
    Checkbox
    )
    "Date"
    <input type="date" …>
    (如果需要时间组件则用
    "datetime-local"
    )
    "unknown"
    <input type="text" …>
    —— 尽力而为,在调用处进行类型转换
  • execute({ … })
    中的值类型必须与
    jsType
    匹配。
    jsType: "number"
    的参数需要传入
    number
    类型,而不是字符串。当使用
    type="number"
    时,输入框的
    .valueAsNumber
    会返回数字类型,但
    <input>.value
    仍然是字符串——如果你读取的是后者,请在调用处进行转换(
    Number(input)
    )。
  • required: true
    的参数不能省略。
    这一点会反映在TypeScript类型中:必填键是必须传入的。
  • displayName
    用于标签显示;绝不要将其作为键名使用。
对于隐式动作,slug与模型的列名(经过slug化处理后)一致。对于自定义SQL动作,slug是动作的作者在Metabase中为SQL参数指定的名称。无论哪种情况,schema都是唯一的可信来源。

Form-side validation — match the main app

表单端验证——与主应用保持一致

The schema exposes
parameter.required
(and
jsType
for type coercion). That's the full validation contract today, matching Metabase's built-in action-execute form. No length checks, no min/max, no format checks — anything more granular comes back as a BE error after submit (see Showing the error message).
Schema is the ONLY source of truth. Read
parameter.required
and wire it — nothing else. Don't add
required
because a field is named
"email"
, don't cap a "name" at 100 chars on a hunch, don't set
type="email"
from the slug. If the schema is silent, the field is unconstrained.
Disable the submit button while the form is invalid. Drive it from the browser's native verdict so
required
is the only rule in play:
tsx
const [isFormValid, setIsFormValid] = useState(false);
const onFormChange = (e: React.FormEvent<HTMLFormElement>) =>
  setIsFormValid(e.currentTarget.checkValidity());

<form onSubmit={...} onChange={onFormChange}>
  <input required={parameter.required} />
  <button type="submit" disabled={isExecuting || !isFormValid}>Create</button>
</form>
No hand-rolled
!name || !email
checks.
schema暴露了
parameter.required
(以及用于类型转换的
jsType
)。这是目前完整的验证约定,与Metabase内置的动作执行表单保持一致。没有长度检查、没有最小值/最大值限制、没有格式校验——任何更细粒度的校验都会在提交后以服务端错误的形式返回(参见显示错误信息)。
Schema是唯一的可信来源。 只需读取
parameter.required
并进行绑定即可,不要额外添加规则。不要因为字段名为
"email"
就添加必填校验,不要凭直觉将
"name"
的长度限制为100字符,不要根据slug就设置
type="email"
。如果schema中没有说明,该字段就是无约束的。
表单无效时禁用提交按钮。 请使用浏览器原生的校验结果来控制,这样就能保证只有
required
规则生效:
tsx
const [isFormValid, setIsFormValid] = useState(false);
const onFormChange = (e: React.FormEvent<HTMLFormElement>) =>
  setIsFormValid(e.currentTarget.checkValidity());

<form onSubmit={...} onChange={onFormChange}>
  <input required={parameter.required} />
  <button type="submit" disabled={isExecuting || !isFormValid}>Create</button>
</form>
不要手动编写
!name || !email
之类的校验逻辑。

Debugging Checklist

调试清单

When an action appears to succeed but the screen doesn't update, or a call fails with a 400:
  1. Log
    result
    ,
    error
    , and
    isExecuting
    after
    await execute(...)
    to confirm the request actually went through and succeeded.
  2. Confirm
    useAction
    was called with the
    defineAction(...)
    export imported from
    actions/
    . Passing the schema entry, an inline object, or a spread copy is a compile error; passing its
    .id
    compiles but leaves
    execute
    untyped and 403s in production, since the authored action is not the one an app's users can run. Calling
    defineAction(...)
    inside the component compiles too, but
    sync-resources
    scans only
    actions/
    , so that action is never copied and also 403s.
  3. Log the object passed to
    execute({ ... })
    . Every key must match a parameter
    slug
    from
    schema.models.<model>.actions.<action>.parameters
    ; every value must match its declared
    jsType
    .
  4. List every data view on the screen that reads from the mutated model. Confirm each one's data hook is mounted ABOVE the action trigger so its refresh callback can be passed down.
  5. Confirm the refresh callback is called AFTER
    await execute(...)
    AND that the refresh itself is awaited. When multiple refreshes apply, confirm they're awaited together (
    Promise.all
    ).
  6. For implicit actions, confirm the right
    implicitKind
    matches what you intend (
    row/create
    vs
    row/update
    vs
    row/delete
    ) — picking the wrong action publishes the wrong write.
  7. If the schema doesn't list an action you expect, the action isn't defined on the instance or the schema file is stale. Regenerate the schema.
当动作看似执行成功但屏幕没有更新,或者调用返回400错误时,请按以下步骤排查:
  1. 在
    await execute(...)
    之后打印
    result
    、
    error
    和
    isExecuting
    ,确认请求确实发出并成功执行。
  2. 确认
    useAction
    的入参是从
    actions/
    目录导入的
    defineAction(...)
    导出结果。传递schema条目、内联对象或展开拷贝会导致编译错误;传递它的
    .id
    虽然能通过编译,但
    execute
    会失去类型保护,并且在生产环境中会返回403错误,因为用户可运行的不是原始编写的动作。在组件内部调用
    defineAction(...)
    也能通过编译,但
    sync-resources
    只会扫描
    actions/
    目录,因此该动作永远不会被同步,同样会返回403错误。
  3. 打印传递给
    execute({ ... })
    的对象。每个键都必须与
    schema.models.<model>.actions.<action>.parameters
    中的参数
    slug
    匹配;每个值的类型都必须与声明的
    jsType
    一致。
  4. 列出屏幕上所有从被修改的模型中读取数据的视图。确认每个视图的数据hook都挂载在动作触发组件的上方,这样它的刷新回调才能向下传递。
  5. 确认刷新回调是在
    await execute(...)
    之后调用的,并且刷新操作本身也被await了。如果有多个刷新操作,确认它们被同时await(使用
    Promise.all
    )。
  6. 对于隐式动作,确认
    implicitKind
    与你的预期一致(
    row/create
    、
    row/update
    还是
    row/delete
    )——选错动作会执行错误的写入操作。
  7. 如果schema中没有列出你预期的动作,说明该动作未在实例上定义,或者schema文件已经过时。请重新生成schema。

Common Mistakes

常见错误

  • Importing
    useAction
    from
    @metabase/embedding-sdk-react
    instead of
    @metabase/embedding-sdk-react/data-app
    . The main entry's hook is the general SDK one and accepts anything, so the definition check never runs.
  • Declaring the action anywhere but root-level
    actions/
    : inline at the hook, wrapped in
    defineAction(...)
    inside a component, or under
    src/actions/
    . Only
    actions/
    is synchronized.
  • Passing
    schema.models.<model>.actions.<action>.id
    to
    useAction
    instead of the
    defineAction(...)
    export.
    execute
    then accepts any
    Record<string, unknown>
    , typos like
    { wrongKey: 1 }
    slip through, and production 403s because the authored action is not the one an app's users can run.
  • Forgetting to refresh after a successful action. The UI keeps rendering stale data with no error or warning.
  • Rendering
    "Failed"
    /
    "Something went wrong"
    /
    String(error)
    instead of the real backend message. Always extract
    error.data.message
    /
    error.data.errors
    (the diagnostic the user needs is in there) and render it verbatim — see Showing the error message.
  • Calling the refresh callback without
    await
    ing it. The modal dismisses or the form clears before fresh data arrives, leaving the user staring at the stale view for a beat.
  • Loading data INSIDE the same component that triggers the action. The hook is below the trigger, so its refresh callback can't be wired up. Lift the data hook to a parent and hand its refresh callback down to the trigger.
  • Passing raw
    <input>
    strings as parameter values when the slug's
    jsType
    is
    "number"
    or
    "boolean"
    . Coerce at the call site.
  • Letting the trigger fire while a previous request is in flight. Drive
    disabled={isExecuting}
    from the hook.
  • Prepending the action's
    result
    directly to a local list to skip the refresh. Server-filled defaults (auto-IDs, timestamps, computed columns, derived join columns) diverge from any client-side guess.
  • Inventing an action because the user asked for behavior the instance doesn't expose. The schema is the catalog of what exists; surface the gap, don't fake the call.
  • Reaching for
    fetch("/api/action/...")
    directly. The sandbox blocks raw network calls to the Metabase origin (raw
    fetch
    /XHR work only for external
    allowed_hosts
    declared in
    data_app.yaml
    ); the only path to actions is
    useAction
    .
  • 从
    @metabase/embedding-sdk-react
    而不是
    @metabase/embedding-sdk-react/data-app
    导入
    useAction
    。主入口的hook是通用SDK版本,接受任何类型的入参,因此永远不会执行动作定义校验。
  • 将动作声明在根目录
    actions/
    之外的任何地方:在hook处内联、在组件内部用
    defineAction(...)
    包裹、或者放在
    src/actions/
    下。只有
    actions/
    目录下的内容会被同步。
  • 将
    schema.models.<model>.actions.<action>.id
    而不是
    defineAction(...)
    的导出结果传递给
    useAction
    。这样
    execute
    会接受任何
    Record<string, unknown>
    类型的参数,像
    { wrongKey: 1 }
    这样的拼写错误会被放过,并且在生产环境中会返回403错误,因为用户可运行的不是原始编写的动作。
  • 动作成功后忘记刷新。UI会继续显示过时的数据,没有任何错误或警告。
  • 渲染
    "Failed"
    /
    "Something went wrong"
    /
    String(error)
    来代替真实的后端错误信息。请始终提取
    error.data.message
    /
    error.data.errors
    (用户需要的诊断信息就在里面)并原样渲染——参见显示错误信息。
  • 调用刷新回调时没有await。模态框关闭或表单清空时,新数据还没到达,导致用户会短暂看到过时的视图。
  • 在触发动作的同一个组件内加载数据。数据hook位于触发组件的下方,因此它的刷新回调无法被绑定。请将数据hook提升到父组件,并将其刷新回调传递给触发组件。
  • 当参数的
    jsType
    是
    "number"
    或
    "boolean"
    时,传递原始的
    <input>
    字符串作为参数值。请在调用处进行类型转换。
  • 在上一个请求还在进行时允许再次触发。请用hook返回的
    isExecuting
    来控制
    disabled
    状态。
  • 为了跳过刷新,直接将动作的
    result
    添加到本地列表的开头。服务器填充的默认值(自动ID、时间戳、计算列、派生的连接列)会与客户端的任何猜测都不一致。
  • 因为用户要求某个实例未提供的功能,就虚构一个动作。schema是现有功能的目录;请暴露这个差距,不要伪造调用。
  • 直接使用
    fetch("/api/action/...")
    调用接口。沙箱会阻止发往Metabase源的原始网络请求(原始的
    fetch
    /XHR仅对
    data_app.yaml
    中声明的外部
    allowed_hosts
    有效);调用动作的唯一途径是
    useAction
    。