nocobase-ui-builder
Installation
SKILL.md
Goal
- Canonical transport is
nb api flow-surfaces. - Use
nbas the only public transport. Ifnbis missing or stale, report the blocked command/env state instead of switching transports. - Keep routing intent-first: open one matching quick-route doc first, not the whole directory.
- When a quick route already matches, stay on it. Do not enumerate the skill directory just to rediscover docs.
- When the task is a partial-match or handoff-only request, answer from this skill's scope boundary directly. Do not inspect runtime, scripts, or helper docs just to justify the handoff.
- Treat one user request that spans several pages as ordered single-page runs.
Router
- whole-page authoring goes through
applyBlueprint,nb api flow-surfaces apply-blueprint, and whole-page-quick.md - localized existing-surface edits go through low-level
flow-surfacesand local-edit-quick.md - reaction work starts with
get-reaction-meta,nb api flow-surfaces get-reaction-meta, writes throughset*Rules, and reaction-quick.md - partial-match or boundary-only requests go through boundary-quick.md first
- After that route is clear, if template / reference /
copyrouting is truly in scope, read template-quick.md first and then templates.md for the full decision matrix. - Do not open tool-shapes.md or helper-contracts.md until you are preparing a real nb body, running the
prepare-writegate, or validating a prepared write body. - If the task involves JS
code,renderer: "js",jsBlock,jsColumn,jsItem, JS actions, charts, orctx.*API questions, read js.md first, then js-surfaces/index.md, then js-snippets/index.md, and only then js-reference-index.md. - Before using a
flow-surfacessubcommand you have not used yet in the current task, runnb api flow-surfaces --helpornb api flow-surfaces <subcommand> --help. - Local helper CLIs are skill-local, not PATH contracts. Invoke
nb-page-preview,nb-runjs,nb-template-decision, andnb-localized-write-preflightthroughnode skills/nocobase-ui-builder/runtime/bin/<helper>.mjsfrom this repo root, or through the equivalent absolute path to this skill. Do not probe bare helper names first.
Hard Rules
- For nb writes, pass the business object itself as the raw JSON body. After whole-page
prepare-write, that business object isresult.cliBody, not the original draft blueprint. Do not wrap that object again. - For a normal single-page request, default to exactly one real tab. Do not add empty tabs or placeholder
markdown/ note / banner blocks unless the user asked for them. - Default blueprint
fields[]entries to simple strings. Upgrade a field entry to an object only whenpopup,target,renderer, or a field-specifictypeis required. - For page authoring, field truth comes from live collection metadata. Prefer
nb api data-modeling collections get --filter-by-tk <collection> --appends fields -j; if that command family is unavailable, usenb api resource list --resource collections --filter '{"name":"<collection>"}' --appends fields -j. Do not usedata-modeling fields list/nb api data-modeling collections fields listas the authoring truth. Any field used in blueprintfields[]must have a non-emptyinterface. For whole-pageapplyBlueprint, recompute the full involved collection set from live metadata on every draft and rebuilddefaults.collectionsfrom scratch instead of reusing stale fragments. For every involved direct collection, always emitpopups.view/popups.addNew/popups.editas{ name, description }, and let anytableblock pull that collection intoaddNewthreshold evaluation even when the blueprint did not spell out anaddNewopener. KeepfieldGroupscollection-only on the target collection, and adddefaults.collections.<collection>.fieldGroupsonly when one of those fixed generated popup scenes should still have more than 10 effective fields after scene filtering; otherwise omitfieldGroups. For association fields, keep every involved relation scope on the same fixedview/addNew/edittrio underdefaults.collections.<sourceCollection>.popups.associations.<associationField>.<action>with the same{ name, description }contract, keyed only by the first relation segment; prepare-write may normalize legacy deeper keys down to that first segment. Do not create per-association or relation-scopedfieldGroups. Never generatedefaults.blocks, and never putblocks,fields,fieldGroups, or layout underdefaults.collections.*.popups. layoutbelongs only ontabs[]or inlinepopup, never on a block object. ForcreateForm,editForm,details, andfilterForm, usefieldsLayoutwhen the blueprint must control the inner field grid directly. Omit page/popuplayoutonly when that tab/popup has at most one non-filter block; otherwise explicit layout is required. For low-levelset-layout, do not reuse public{ rows: [[{ key, span }]] }syntax: runtimerowsisRecord<string, string[][]>, each cell array stacks live childuids,[[uidA], [uidB]]means two columns, and[[uidA, uidB]]means one stacked column.- For
createForm,editForm, anddetails, once the block contains more than 10 real fields, use explicitfieldGroupsinstead of one flatfields[]list. Do not treat manualdivideritems as a substitute, and do not combinefieldGroupswithfieldsLayout. - If clicking a shown record or relation record should open details, prefer a field popup. Use a button or action column only when the request explicitly asks for one.
- If visible same-title destination menu groups already exist, never create another same-title group just to avoid ambiguity and never choose one locally. In that multi-match case, require explicit
navigation.group.routeIdbefore write. Only zero-match create and one-match title-only reuse may proceed withoutrouteId. - In
applyBlueprint create, any newly creatednavigation.groupand any top-level or second-levelnavigation.itemmust carry one valid Ant Design icon name. Whennavigation.itemis attached under an explicit existingnavigation.group.routeId, keep an icon by default but do not assume the local preview can prove whether that live target is already third-level or deeper. navigation.group.routeIdand desktop-routeidare navigation locators only. When follow-up localized work or explicit inspection is needed after create/init or successful whole-pageapplyBlueprint, normalize topageSchemaUidfor page-levelflow-surfaces get, and only use liveuidvalues returned byget/describe-surface/ create responses forcatalog,context,get-reaction-meta,compose,configure,add*, orremove*. Never pass a desktop-routeidastarget.uid.- Before the first real whole-page
applyBlueprint, run the local prepare-write gate (node skills/nocobase-ui-builder/runtime/bin/nb-page-preview.mjs --stdin-json --prepare-writeorprepareApplyBlueprintRequest(...)) and show one ASCII-first prewrite preview from the same draft blueprint. Treat that helper as a local shape/threshold gate only: it does not fetch live collection metadata by itself.collectionMetadatastays caller-supplied and is required for any blueprint containing a data-bound block (a block withcollection,resource,binding,dataSourceKey,associationPathName, orassociationField); missing or empty metadata must fail prepare-write withmissing-collection-metadata. It accepts omitted data-surfacefilteractions, but every direct non-template publictable/list/gridCard/calendar/kanbanblock must still carry a non-empty block-leveldefaultFilter; if afilteraction also providessettings.defaultFilter, prepare-write validates that explicit action payload too. Publickanbanmain blocks may keepfields[], but must not carryfieldGroups,fieldsLayout, orrecordActions; allowed main-block actions arefilter,addNew,popup,refresh, andjs. For block settings, numericheightmeans a fixed height and must be paired withheightMode: "specifyValue"; prepare-write auto-adds that mode whensettings.heightis present andsettings.heightModeis omitted, including popup blocks. WithcollectionMetadata, it validates fixed defaults completeness for every involved scope: requireddefaults.collectionsentries, required popup{ name, description }entries for the fixedview/addNew/edittrio, and requiredfieldGroupswhen any fixed generated popup scene still has more than 10 effective fields. For artifact-only drafts with no real write, draftprewrite-preview.txtdirectly from the same draft blueprint instead of attempting the local helper CLI. - Treat the local prepare-write helper as the authority for normalized local write shape. If it auto-adds, rewrites, or normalizes fields in ways that are within the helper's expected contract, keep that result as-is. Whole-page work includes whole-page create / replace, one route-backed tab full build, complex multi-block pages, nested-popup pages, and pages with multiple reaction families. Pre-write reads, metadata fetch, preview, and
prepare-writeare allowed, but the first mutating write in that route must beapplyBlueprint. For the first real whole-page write,prepare-writeis mandatory and the only nb write body isresult.cliBody; do not send the original draft blueprint toapplyBlueprint. For whole-pageapplyBlueprintcreate / replace and same-blueprintreaction.items[], a successfulapplyBlueprintresponse is the default stop point. Run follow-upgetonly when follow-up localized work or explicit inspection needs live structure. Without that extra readback, report the write from the success response and request intent rather than as a normalized persisted subtree. - Treat default values, computed values, field/block/action state, and show/hide as reaction work first. Do not guess raw configure keys.
- If live readback shows an existing template reference and the requested change touches template-owned content, default to the template source. Keep host/openView config edits local. Page-scoped wording is not local-only intent, so do not auto-detach to
copy; clarify before writing when scope is unresolved. - In testing or multi-agent runs, do not perform destructive cleanup unless the user explicitly asked for deletion.
- When you actually have persisted readback to summarize for the user or for local helper artifacts, prefer one stable public summary with normalized type labels such as
table,details,editForm,filterForm, andcreateForm; do not rely on raw model names alone. For page-level create / replace, keeppage.pageSchemaUid,page.pageTitle, andpage.menuGroupTitleexplicit in that summary. When a scenario spans multiple pages, use the same canonical page identity keys underpages.*, and usetypefor concrete summary nodes such astables.*,lists.*, andforms.*; reserveblockTypesfor aggregate arrays such asroot.blockTypesorpopups.*.blockTypes. Keep root actions underroot.actionTitlesinstead of leavingrecordActionTitlesas the only proof. - For reaction work, pick the final block/action target only after
get-reaction-metaproves the required source path is available in that scene. On targets that expose multiple capabilities, select the write slot by matchingkindfirst and then reuse that exact capability fingerprint; do not copy a nearby fingerprint from anotherkind. If the current target cannot expose the needed path, move the target or restructure the page/popup first instead of writing a guessed rule to an unsupported host. - Resolve filter wording before choosing structure. For table / list / gridCard / calendar / kanban-like data surfaces, ambiguous “筛选 / filter” requests default to the same host's block-level
filteraction/button, not a separate filter block. Treat “搜索 / search” that way only when the request explicitly adds search to a table / list / gridCard / calendar / kanban / card-like host, including wording such as “支持搜索 / 带搜索 / 可搜索 / searchable”; page-noun wording such as “搜索页 / 搜索结果页 / 搜索门户 / 搜索列表页” should stay page intent, not filter intent. Route树筛选 / 树状筛选 / tree filter / tree filter block / 树形筛选区块directly toTreeBlockModel, notFilterFormBlockModel; read references/blocks/tree.md before writing it. Route分析看板 / dashboard / trend / KPI / 概览to chart/grid-card insight paths by default; route toKanbanBlockModelonly when kanban cues such as看板区块 / kanban / pipeline / status columns / 拖拽 / 泳道 / backlogare present. Plain看板alone does not override analytics intent. Do not create a newfilterFormby default. Read references/aliases.md first. Open references/blocks/filter-form.md and keep a realfilterFormin the first-pass blueprint only when the user explicitly asks for a filter/search block, form, or query area and the phrase is not a tree-filter request; then include stable filter items,submit/resetactions, and same-blueprint stringtargetblock keys instead of low-leveldefaultTargetUidor raw block settings payloads. - When you author one localized
compose/add-block/add-blocksbody locally, validate that body first with the local localized preflight helper (node skills/nocobase-ui-builder/runtime/bin/nb-localized-write-preflight.mjs --operation <compose|add-block|add-blocks> --stdin-json) orrunLocalizedWritePreflight(...). Treat it as an explicit local validator for skill-authored low-level payloads, not as a transport wrapper: it requires caller-suppliedcollectionMetadatafor data-bound payloads, reportsmissing-collection-metadatawhen metadata is absent, keeps direct non-template publictable/list/gridCard/calendar/kanbanblocks fail-closed on missing or empty block-leveldefaultFilter, validates metadata-aware filter coverage, rejects unsupportedkanban/calendarmain-block sections, and auto-addsheightMode: "specifyValue"whensettings.heightor configurechanges.heightis present without an explicit mode. After a successful helper run, sendresult.cliBodyin the later explicitnb api flow-surfaces ...call. The backend runtime remains compatibility-tolerant; this stricter requirement belongs to the local skill-side validator contract. - If a first-pass whole-page write still leaves
filterFormas an empty shell after a successfulapplyBlueprint, treat it as an explicit local/live gap and keep any low-leveladdBlock/addAction/addFieldrepair narrowly scoped. If the firstapplyBlueprintfails with a verifiedfilterForm-specific shape/runtime error, repair the blueprint from that error, rerunprepare-writeand preview, and retry blueprint-only up to 5 rounds. Do not switch to low-level writes during those pre-success retries. After 5 failed rounds, report the latest blueprint / preview / error evidence. - If a create/edit form helper or reference depends on
formValues.*, inspect catalog /get-reaction-metabefore choosing the host. When that live scene exposesfields/actions/nodebut notblocks, model the helper as ajsItemor other field-like helper inside the same form scene, not as a standalone block; for current JSItem targets, implement hide/show by renderingnulluntil the form value is present instead of assumingsetFieldStatecan target the JSItem. When that render-null pattern is used successfully, treat it as a configured helper toggle in readback/evidence instead of marking the helper outcome false only becausefieldLinkagecannot target the JSItem itself. - Treat pages with multiple work areas, filter/search blocks, nested popups, or multiple reaction families as complex whole-page requests, not as a separate router path. They still stay on whole-page-quick.md and still prefer one
applyBlueprintrequest. - For those complex whole-page requests, first-pass blueprint generation should include the structural blocks, inline popups, and top-level
reaction.items[]together. Do not split the page into root-shell / popup / reaction phases just because the page is large. - Use low-level
get-reaction-meta+set*Rulesoradd*repair only for localized edits on an existing live page, or after a successful whole-pageapplyBlueprintwhen an explicit local/live gap still needs narrowly scoped repair. Before one whole-pageapplyBlueprintsucceeds, do not usecreateMenu,createPage,compose,configure,update-settings,add*,move*,remove*, orset*Rules. If a whole-pageapplyBlueprintfails before first success, repair the blueprint from the error, rerunprepare-writeand preview, and retry blueprint-only up to 5 rounds. Do not switch to low-level writes during those pre-success retries. After 5 failed rounds, report the latest blueprint / preview / error evidence. - Stay env-neutral in the general skill contract. Use the current configured CLI env or explicit runtime flags instead of hard-coding local aliases or fixed URLs.
Read Paths
- Route unclear: references/index.md
- Whole-page draft/create/replace from business intent: whole-page-quick.md
- Localized existing-surface edit: local-edit-quick.md
- Whole-page or localized reaction change: reaction-quick.md
- Partial-match handling and narrow handoff reports: boundary-quick.md
- Reuse, template selection, or existing template reference edits: template-quick.md
- Write-time helper CLIs / prewrite gate / helper return shapes for real writes: helper-contracts.md
- JS or chart work: js.md or chart.md
Scope & Handoff
- Handle only Modern page (v2) menu/page/tab/popup/content surfaces and the block / field / action / layout / reaction work inside them.
- For partial-match or boundary-report tasks, keep the Modern-page slice narrow and write the handoff report directly from this boundary list. Do not inspect runtime or scripts unless the request is explicitly about those mechanics.
- Hand off ACL / route permissions / role permissions to
nocobase-acl-manage. - Hand off collection / field / relation authoring to
nocobase-data-modeling. - Hand off workflow create / update / revision / execution to
nocobase-workflow-manage.