fcode-ui-triggers

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Factorial Code — UI triggers

Factorial Code — UI 触发器

A UI trigger exposes a marketplace app's process as a button inside Factorial (a page header, an actions dropdown, …). Factorial pages declare locations; an installed app's process claims a location, and Factorial renders one button per claiming process. Clicking it runs the process — or opens its form. It is one entry point of a Factorial Action: its settings live in the
factorial.uiTrigger
sub-block of the process's
metadata.json
, with
awaitResult
at the top level of the
factorial
block (the whole block, the legacy mirror and the Factorial One / backend entry points are in
fcode-factorial-actions
; field reference in
fcode-cli
).
UI 触发器可将 marketplace 应用的流程暴露为 Factorial 内的一个按钮(位于页面页眉、操作下拉菜单等处)。Factorial 页面会声明 location;已安装应用的流程可声明一个 location,Factorial 则为每个声明该 location 的流程渲染一个按钮。点击按钮会运行流程,或者打开其表单。它也是 Factorial Action 的一种入口:相关设置位于流程
metadata.json
的
factorial.uiTrigger
子块中,而
awaitResult
位于
factorial
块顶层(整个块、旧版镜像以及 Factorial One / 后端入口点均见
fcode-factorial-actions
;字段参考见
fcode-cli
)。

Gotchas

注意事项

  • Only installed marketplace apps get buttons. Factorial lists the triggers of the apps the company has installed, read from their deploy workspaces. A process in a plain workspace never shows up, and a trigger reaches customers the way the rest of the app does: dev → prod → deploy through a release.
  • The location id must come from the Factorial team that owns the page. Locations are declared in Factorial's code (e.g.
    calendar.header.admin
    ), not on the platform. The platform accepts any non-blank
    locationId
    (≤ 200 chars) — an unknown one is silently dropped from the listing, so a typo means "no button", not an error.
  • The location decides the contract, not the process: which context params are forwarded to the process, which keys of a synchronous result reach the page, and whether the location is
    single
    (one installed app at a time). Ask for those three things along with the id.
  • awaitResult
    lives on the
    factorial
    block and defaults to
    true
    (synchronous).
    The legacy
    uiTrigger.awaitResult
    defaulted to fire-and-forget, and the mirror copies the new value over it — so a button moved into
    factorial
    runs synchronously unless you write
    "awaitResult": false
    . Keep it
    true
    only for fast work (< 50 s) whose outcome the user must see; with
    false
    the click queues an execution, the user sees "action started" and the return value is never shown.
  • A form-enabled process opens its form instead of running. With
    factorial.form.enabled
    (or the legacy
    form.enabled
    ) on, the button opens the form in a dialog inside Factorial and
    awaitResult
    is ignored.
  • A trigger-opened form goes through Factorial, not straight to the platform. The dialog reads the schema and submits it through Factorial's own backend, which carries the clicking user's identity. The form's own
    authMode
    still governs anyone reaching it by its public URL (
    fcode-forms
    ); it does not govern the dialog.
  • company_id
    ,
    triggered_from_location
    and
    access_id
    are trustworthy on both paths.
    Factorial injects them server-side on every invocation, form or not, and the location's forwarded params overwrite whatever the browser sent. Authorizing on them is safe. (This was not true of the old form path, which passed them as client-editable pre-filled fields.)
  • Who may click is decided by
    requiredPolicies
    , and it is enforced.
    Factorial checks
    factorial.requiredPolicies
    (
    fcode-factorial-actions
    ) against the clicking user before it runs anything, on top of a policy-scoped read of the location's resource. An action with no policies listed is open to anyone who can reach the button.
  • Uncaught errors show a generic message. A throw / crash reaches the user as "The action could not be completed" with no detail. Return
    { errors: [...] }
    for anything the user should read.
  • Changes take up to five minutes to appear — Factorial caches the trigger listing in the browser.
  • 只有已安装的 marketplace 应用才会显示按钮。 Factorial 会列出公司已安装应用的触发器,列表读取自这些应用的 deploy workspace。普通 workspace 中的流程永远不会显示;触发器会与应用的其他部分通过相同路径交付给客户:dev → prod → 通过 release 进行 deploy。
  • location id 必须来自拥有该页面的 Factorial 团队。 Location 是在 Factorial 代码中声明的(例如
    calendar.header.admin
    ),而不是在平台上声明。平台接受任何非空
    locationId
    (≤ 200 个字符)——未知 location 会被静默地从列表中移除,因此拼写错误意味着“没有按钮”,而不是报错。
  • 决定契约的是 location,而不是流程:哪些上下文参数会转发给流程、同步结果的哪些键可以到达页面,以及该 location 是否为
    single
    (同一时间只能有一个已安装应用)。索取 location id 时,也应一并确认这三项。
  • awaitResult
    位于
    factorial
    块顶层,默认值为
    true
    (同步)。
    旧版
    uiTrigger.awaitResult
    默认为 fire-and-forget;镜像会将新值复制到旧字段中——因此,移入
    factorial
    的按钮默认会同步运行,除非显式写入
    "awaitResult": false
    。仅当任务执行速度快(< 50 秒)且用户必须看到结果时,才应保持
    true
    ;设为
    false
    时,点击操作会排队执行,用户只会看到“操作已启动”,返回值也不会显示。
  • 启用表单的流程会打开表单,而不是直接运行。 开启
    factorial.form.enabled
    (或旧版
    form.enabled
    )后,按钮会在 Factorial 内的对话框中打开表单,并且
    awaitResult
    会被忽略。
  • 通过触发器打开的表单会经过 Factorial,而不是直接提交给平台。 对话框读取 schema,并通过 Factorial 自己的后端提交;该后端会携带点击用户的身份。表单自身的
    authMode
    仍负责控制通过公开 URL 访问表单的人(
    fcode-forms
    ),但它不控制该对话框。
  • 无论走哪条路径,
    company_id
    、
    triggered_from_location
    和
    access_id
    都可信。
    Factorial 会在每次调用时通过服务端注入这三个值,无论是否使用表单;location 转发的参数会覆盖浏览器发送的任何同名字段。基于这些字段进行授权是安全的。(旧版表单路径并非如此;它会将这些字段作为客户端可编辑的预填字段传入。)
  • 谁可以点击由
    requiredPolicies
    决定,并且会被强制执行。
    在运行任何操作之前,Factorial 会针对点击用户检查
    factorial.requiredPolicies
    (
    fcode-factorial-actions
    ),此外还会对 location 的
    resource
    执行受策略范围限制的读取。未列出任何策略的 Action,对任何能够访问按钮的人都是开放的。
  • 未捕获的错误只显示通用消息。 抛出的异常或崩溃会以“The action could not be completed”呈现给用户,不含任何细节。需要让用户阅读的信息请返回
    { errors: [...] }
    。
  • 更改最多需要五分钟才会显示——Factorial 会在浏览器中缓存触发器列表。

Declare a trigger

声明触发器

In
processes/<slug>/metadata.json
, then
fcode push
(or the Trigger from Factorial section of the process page in the console):
json
{
  "name": "Sync report",
  "tags": ["acme"],
  "factorial": {
    "enabled": true,
    "uiTrigger": {
      "enabled": true,
      "locationId": "compensations.cycle.header",
      "label": "Sync to Acme",
      "icon": "Refresh"
    }
  }
}
FieldMeaning
factorial.enabled
Master switch of the whole action; the button needs it on
factorial.awaitResult
Defaults to
true
: run synchronously and show the outcome. Write
false
for fire-and-forget
uiTrigger.enabled
Turns the button on.
locationId
is required when
true
uiTrigger.locationId
The Factorial location the button renders at, as given by the page's owning team
uiTrigger.label
Button text. Plain text, or
fcode.i18n("key")
tokens (below) — the only i18n field of the block
uiTrigger.icon
One of the allowlisted names (below); omit for a text-only button
The legacy top-level
uiTrigger
block (
enabled
,
locationId
,
label
,
icon
,
awaitResult
) still round-trips and is kept mirrored with
factorial.uiTrigger
— an old file keeps working, and
fcode pull
still writes
"uiTrigger": { "enabled": false }
on every process. Write
factorial
in new work; when both are present
fcode push
sends
factorial
(mirror rules in
fcode-factorial-actions
).
An app may declare several triggers at one location — each renders its own button — but a location marked
single
in Factorial admits one installed app: installing a second app that claims it fails with a conflict naming the first. Don't claim a
single
location from an app meant to coexist with others.
在
processes/<slug>/metadata.json
中进行声明,然后执行
fcode push
(或在控制台的流程页面中操作 Trigger from Factorial 区域):
json
{
  "name": "Sync report",
  "tags": ["acme"],
  "factorial": {
    "enabled": true,
    "uiTrigger": {
      "enabled": true,
      "locationId": "compensations.cycle.header",
      "label": "Sync to Acme",
      "icon": "Refresh"
    }
  }
}
字段含义
factorial.enabled
整个 Action 的总开关;按钮需要启用它
factorial.awaitResult
默认为
true
:同步运行并显示结果。设为
false
可实现 fire-and-forget
uiTrigger.enabled
启用按钮。设为
true
时必须提供
locationId
uiTrigger.locationId
按钮渲染所在的 Factorial location,由页面所属团队提供
uiTrigger.label
按钮文本。可使用纯文本,或
fcode.i18n("key")
token(见下文)——这是该块中唯一可使用 i18n 的字段
uiTrigger.icon
允许列表中的一个名称(见下文);省略则创建纯文本按钮
旧版顶层
uiTrigger
块(
enabled
、
locationId
、
label
、
icon
、
awaitResult
)仍支持往返读写,并会与
factorial.uiTrigger
保持镜像同步——旧文件仍可正常工作,而且
fcode pull
仍会为每个流程写入
"uiTrigger": { "enabled": false }
。新开发应写入
factorial
;两者同时存在时,
fcode push
会发送
factorial
(镜像规则见
fcode-factorial-actions
)。
一个应用可以在同一 location 声明多个触发器——每个触发器都会渲染自己的按钮——但 Factorial 中标记为
single
的 location 只允许一个已安装应用:安装第二个声明该 location 的应用时,会因与第一个应用冲突而失败。不要让需要与其他应用共存的应用声明
single
location。

What the process receives

流程会接收到什么

The click runs the process with
fcode.context.parameters
set to:
  • the location's forwarded context params — e.g. the id of the record the page shows (
    cycle_id
    ), exactly the keys the location allowlists;
  • company_id
    — the Factorial company whose user clicked (trusted: injected under a shared secret, overwriting anything the browser sent);
  • triggered_from_location
    — the location id (trusted, same way);
  • access_id
    — the Factorial access (the clicking user's membership in that company) as a string (trusted, same way).
A context-less location (a header button with no record in scope) forwards no params at all — only the three trusted keys. The clicking user's locale is passed as the execution locale, so
fcode.i18n
in the process speaks their language (
fcode-i18n
).
javascript
const { company_id, access_id, triggered_from_location, cycle_id } = fcode.context.parameters;
if (!cycle_id) throw new Error("cycle_id is required");
点击会运行流程,并将
fcode.context.parameters
设为:
  • location 的转发上下文参数——例如页面所显示记录的 id(
    cycle_id
    )——键名完全遵循 location 的允许列表;
  • company_id
    ——点击用户所属的 Factorial 公司(可信:通过共享密钥注入,并覆盖浏览器发送的任何同名字段);
  • triggered_from_location
    ——location id(可信,注入方式同上);
  • access_id
    ——Factorial access,即点击用户在该公司的成员身份,以字符串形式提供(可信,注入方式同上)。
无可用上下文的位置(例如没有范围内记录的头部按钮)不会转发任何参数——只有上述三个可信键。点击用户的 locale 会作为执行 locale 传入,因此流程中的
fcode.i18n
会使用用户的语言(
fcode-i18n
)。
javascript
const { company_id, access_id, triggered_from_location, cycle_id } = fcode.context.parameters;
if (!cycle_id) throw new Error("cycle_id is required");

Return a result (synchronous triggers)

返回结果(同步触发器)

With
awaitResult: true
Factorial waits for the execution and hands the page whatever you returned, unchanged. There is no envelope to wrap it in, and nothing is filtered out on the way.
javascript
// Success — the whole object reaches the page's onSuccess handler.
return { synced: 42, report_url: "https://acme.example/reports/7" };
  • A success status is always a success, whatever the body says. Returning an object with an
    errors
    key in it does not make it a failure.
  • Signal an expected failure with an error status, not with the body. Throw
    fcode.error(...)
    , or return an error status from an HTTP-shaped process; the body travels intact so the page can read your message.
  • A throw with no message, or exceeding the synchronous budget (about 50 s; the execution may still finish in the background) → a generic error the user can't act on.
Matching what the entry point expects is the process author's job: a form reads its own conventions (
message
,
formErrors
,
nextProcessId
) while a plain button just shows a toast, so a process exposed as both does not get one portable result shape.
result_keys
on the location is informational — it describes what the page intends to read, and nothing filters by it.
With
awaitResult
off the return value is only visible in the execution log; long work belongs there, or behind
fcode.processes.run(...)
from a quick synchronous trigger (see
fcode-javascript
/
fcode-python
).
当
awaitResult: true
时,Factorial 会等待执行完成,并将你返回的任何内容原样交给页面。无需使用外层 envelope 包装,也不会在传输过程中过滤任何内容。
javascript
// Success — the whole object reaches the page's onSuccess handler.
return { synced: 42, report_url: "https://acme.example/reports/7" };
  • 成功状态始终表示成功,无论响应体内容如何。返回带
    errors
    键的对象并不会使其成为失败。
  • 要表示预期内的失败,应使用错误状态,而不是响应体。 抛出
    fcode.error(...)
    ,或从 HTTP 形态的流程返回错误状态;响应体会原样传递,因此页面可以读取你的消息。
  • 未带消息的异常,或超过同步执行时限(约 50 秒;执行仍可能在后台完成)→ 用户无法据此采取行动的通用错误。
返回结果应匹配入口点的预期,这由流程作者负责:表单按照自己的约定读取结果(
message
、
formErrors
、
nextProcessId
),而普通按钮只显示 toast,因此同时以两种方式暴露的流程无法获得一种可移植的统一结果结构。location 上的
result_keys
仅作说明——它描述页面打算读取的内容,不会据此过滤任何内容。
关闭
awaitResult
后,返回值只能在执行日志中查看;耗时较长的任务应放在那里执行,或者由快速同步触发器通过
fcode.processes.run(...)
启动(参见
fcode-javascript
/
fcode-python
)。

Form-backed triggers

基于表单的触发器

Enable the form as usual (
fcode-forms
) and the button opens it in a dialog inside Factorial, rendered with the f0 theme, pre-filled with the forwarded params. Whether a click opens the dialog or runs the process comes from the action's entry points: declaring both
form
and
uiTrigger
opens the form,
uiTrigger
alone runs it.
The submission goes through Factorial's backend, which injects
company_id
,
triggered_from_location
and
access_id
server-side — declare them in
parametersSchema.json
(a
hidden
widget) if the process reads them. The result follows the form conventions (
message
,
formErrors
,
nextProcessId
, …); a multistep form follows each step's
nextProcessId
inside the same dialog.
json
{
  "name": "Export cycle",
  "factorial": {
    "enabled": true,
    "form": { "enabled": true },
    "uiTrigger": { "enabled": true, "locationId": "compensations.cycle.header", "label": "Export…", "icon": "Download" }
  }
}
像往常一样启用表单(
fcode-forms
)后,按钮就会在 Factorial 内的对话框中打开它,使用 f0 主题渲染,并预先填入转发的参数。点击后是打开对话框还是运行流程,取决于 Action 的入口点配置:同时声明
form
和
uiTrigger
会打开表单,仅声明
uiTrigger
则运行流程。
提交会经过 Factorial 的后端,该后端会在服务端注入
company_id
、
triggered_from_location
和
access_id
——如果流程会读取这些参数,应在
parametersSchema.json
中声明它们(使用
hidden
widget)。结果遵循表单约定(
message
、
formErrors
、
nextProcessId
等);多步骤表单会在同一对话框中按照每一步的
nextProcessId
继续执行。
json
{
  "name": "Export cycle",
  "factorial": {
    "enabled": true,
    "form": { "enabled": true },
    "uiTrigger": { "enabled": true, "locationId": "compensations.cycle.header", "label": "Export…", "icon": "Download" }
  }
}

Files in a trigger form

触发器表单中的文件

A file field works, and the file does not travel through Factorial. Declare the parameter with
x-fcode-file
listing the extensions it takes:
json
"msj_file": {
  "title": "FIE file",
  "type": "string",
  "x-fcode-file": { "accept": [".msj"] },
  "ui": { "ui:widget": "file", "ui:options": { "accept": ".msj" } }
}
On submit, Factorial asks the platform for an upload grant per file, PUTs the file straight to Factorial Code storage, and sends the resulting
fcode.storage://
path as the parameter value. The process reads it as it would any stored file (
fcode-javascript
/
fcode-python
).
  • The grant is issued at submit, not with the schema — the platform checks the real file name against
    accept
    and signs its content type, and neither is known before the user picks a file. A name outside
    accept
    is refused there.
  • Uploaded files are removed once they are 48 hours old. Copy anything the process needs for longer elsewhere in storage.
文件字段可以正常使用,而且文件不会经由 Factorial 传输。使用
x-fcode-file
声明参数及其允许的文件扩展名:
json
"msj_file": {
  "title": "FIE file",
  "type": "string",
  "x-fcode-file": { "accept": [".msj"] },
  "ui": { "ui:widget": "file", "ui:options": { "accept": ".msj" } }
}
提交时,Factorial 会为每个文件向平台申请上传 grant,将文件直接 PUT 到 Factorial Code storage,并把生成的
fcode.storage://
路径作为参数值发送。流程可以像读取任何已存储文件一样读取它(
fcode-javascript
/
fcode-python
)。
  • Grant 是在提交时签发的,而不是随 schema 一起签发——平台会根据真实文件名检查
    accept
    并为其 content type 签名,而这两者在用户选择文件之前都未知。不符合
    accept
    的文件名会在该阶段被拒绝。
  • 上传的文件满 48 小时后会被删除。如果流程需要更长时间保存相关内容,请将其复制到其他 storage 位置。

Labels and i18n

标签与 i18n

label
may embed
fcode.i18n("key")
tokens — the same tokens a form schema uses — resolved with each user's locale from the workspace's locale files, with the primary-locale fallback and never a blank label (model, files and syntax in
fcode-i18n
):
json
"factorial": {
  "enabled": true,
  "uiTrigger": { "enabled": true, "locationId": "calendar.header.admin", "label": "fcode.i18n(\"acme.sync.button\")" }
}
label
可以嵌入
fcode.i18n("key")
token——与表单 schema 使用的 token 相同——这些 token 会根据每个用户的 locale 从 workspace 的 locale 文件中解析,并使用主 locale 作为回退,且绝不会得到空标签(模型、文件和语法见
fcode-i18n
):
json
"factorial": {
  "enabled": true,
  "uiTrigger": { "enabled": true, "locationId": "calendar.header.admin", "label": "fcode.i18n(\"acme.sync.button\")" }
}

Icons

图标

Factorial renders only these names (case-insensitive); anything else falls back to a text-only button, and the console shows the same list as a picker:
Bell
,
Calendar
,
Chart
,
Document
,
Download
,
ExternalLink
,
Globe
,
Graph
,
Link
,
Play
,
Refresh
,
Send
,
Settings
,
Sparkles
,
Star
,
Upload
.
Factorial 仅渲染以下名称(不区分大小写);其他名称都会回退为纯文本按钮,控制台也会以选择器形式显示同一列表:
Bell
,
Calendar
,
Chart
,
Document
,
Download
,
ExternalLink
,
Globe
,
Graph
,
Link
,
Play
,
Refresh
,
Send
,
Settings
,
Sparkles
,
Star
,
Upload
.

How Factorial renders them (for Factorial engineers)

Factorial 的渲染方式(面向 Factorial 工程师)

The Factorial side is generic — no per-app code. In the
factorial
monolith:
  • A location is a YAML file under a component's
    app/fcode_locations/
    (
    id
    , optional
    resource
    +
    param_key
    for the record in scope,
    forward_params
    ,
    result_keys
    ,
    single
    ,
    legacy_ui
    ), validated by a CI spec.
    calendar/header_admin.yml
    is the reference.
  • The page renders
    <FactorialCodeTrigger locationId="…" params={{ cycle_id }} onSuccess onError />
    from
    frontend/src/modules/factorialCode/components/FactorialCodeTrigger
    . It draws one button per claiming trigger (F0
    outline
    button, or the legacy design-system button for
    legacy_ui
    locations), renders nothing while loading or when no installed app claims the location, and owns the form dialog. The underlying hooks (
    useFactorialCodeTriggers
    for the listing,
    useFactorialCodeTriggerActivation
    for the click) are exported for data-driven hosts such as dropdown menus.
  • Clicks go through
    FactorialCode::Interactors::InvokeAction
    : it checks the action's
    requiredPolicies
    (
    RequiredPoliciesAuthorization
    , an OR of AND groups) and a policy-scoped read of the location's
    resource
    , forwards only
    forward_params
    , injects
    company_id
    /
    access_id
    /
    triggered_from_location
    over anything the browser sent, and returns the process's answer unchanged. The SPA addresses an action by its opaque
    action_id
    ; no process slug reaches the browser. Everything is gated by the Factorial Code feature flag.
App developers don't touch any of this; they need the location's id and contract from the team that owns the page.
Factorial 侧采用通用实现——不需要为每个应用编写代码。在
factorial
monolith 中:
  • Location 是组件
    app/fcode_locations/
    下的一个 YAML 文件(
    id
    、用于表示范围内记录的可选
    resource
    +
    param_key
    ,以及
    forward_params
    、
    result_keys
    、
    single
    、
    legacy_ui
    ),并由 CI spec 校验。
    calendar/header_admin.yml
    是参考实现。
  • 页面从
    frontend/src/modules/factorialCode/components/FactorialCodeTrigger
    渲染
    <FactorialCodeTrigger locationId="…" params={{ cycle_id }} onSuccess onError />
    。它会为每个声明该 location 的触发器渲染一个按钮(使用 F0
    outline
    按钮;对于
    legacy_ui
    location,则使用旧版 design-system 按钮),在加载期间或没有已安装应用声明该 location 时不渲染任何内容,并负责管理表单对话框。底层 hooks(用于获取列表的
    useFactorialCodeTriggers
    ,用于处理点击的
    useFactorialCodeTriggerActivation
    )会导出给下拉菜单等数据驱动型宿主使用。
  • 点击会通过
    FactorialCode::Interactors::InvokeAction
    :它会检查 Action 的
    requiredPolicies
    (
    RequiredPoliciesAuthorization
    ,由 AND 组构成的 OR)以及 location 的
    resource
    是否允许在当前策略范围内读取,只转发
    forward_params
    ,用
    company_id
    /
    access_id
    /
    triggered_from_location
    覆盖浏览器发送的任何同名字段,并原样返回流程的响应。SPA 使用不透明的
    action_id
    寻址 Action;浏览器不会收到任何流程 slug。所有行为均受 Factorial Code feature flag 控制。
应用开发者无需接触这些实现;他们只需从拥有页面的团队获取 location id 和对应契约。

Checklist before shipping

发布前检查清单

  1. Location id, forwarded params and
    single
    -ness confirmed with the owning Factorial team.
  2. factorial.awaitResult
    decided explicitly —
    true
    (the default) only for fast, user-visible outcomes;
    false
    otherwise.
  3. factorial.requiredPolicies
    filled in, or deliberately left empty because the button is open to anyone who can reach it.
  4. A form-backed trigger declares the pre-filled keys it reads, and any file parameter carries
    x-fcode-file
    .
  5. Tested from a dev installation in Factorial, then promoted to
    prod-
    and released so the deploy workspaces pick it up (
    fcode-release
    ,
    fcode-ama
    ).
  1. 已与拥有页面的 Factorial 团队确认 location id、转发的参数以及是否为
    single
    。
  2. 明确决定
    factorial.awaitResult
    ——仅当任务执行速度快且结果需要对用户可见时使用
    true
    (默认值),否则使用
    false
    。
  3. factorial.requiredPolicies
    已填写,或明确保留为空,表示按钮对任何能够访问它的人开放。
  4. 基于表单的触发器声明其读取的预填键,所有文件参数均带
    x-fcode-file
    。
  5. 先在 Factorial 中通过开发安装进行测试,再提升到
    prod-
    并发布,使 deploy workspace 能够读取到变更(
    fcode-release
    、
    fcode-ama
    )。