协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
workfront-ui-extension【开发与工程】Use when building or editing the React/Spectrum front-end SPA of a Workfront App Builder extension. Reach for this whenever the user is: registering or changing extension points in `ExtensionRegistration` — a Main Menu button, a left-panel (`secondaryNav`) item for a specific Workfront object type (Project, Task, Issue, Portfolio, Program), or a cust…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
ExtensionRegistration — a Main Menu button, a left-panel (secondaryNav) item for a specific Workfront object type (Project, Task, Issue, Portfolio, Program), or a custom-form widget with specific height and width; adding a new route in App.js to match an extension point URL; reading the Workfront shared context to get the current user, objCode, objID, or hostname; calling a Runtime action from the SPA via actionWebInvoke; or debugging a widget or route that renders blank after being registered. Never call Workfront or Adobe APIs directly from the SPA — all API calls belong in a Runtime action (see workfront-actions)."
license: Apache-2.0Part of the appbuilder-workfront family. This is the front end — the app screens the user sees (the "SPA"). It registers extension points (the spots where the app appears in Workfront) and calls Runtime actions (the cloud back end) for all data.
This skill is the Workfront-specific front end (extension points, WF shared context). For generic React/Spectrum patterns (pages, forms, data tables, dialogs, navigation) and ExC Shell / AEM UI surfaces, use
appbuilder-ui-scaffolder. A ready-to-edit registration example is inassets/ExtensionRegistration.example.js.
register() call wires everything up; individual views use attach().sharedContext + getWFInstanceUrl() — Workfront supplies the signed-in user and instance; don't build a login.actionWebInvoke only — the browser must never call Workfront directly (/attask/api/…). Back-end code lives in workfront-actions.{ data, error } — always check error before using data.register() takes id at the top level (a non-empty slug identifying the extension); the extension points go inside methods. Each item's url must map to a route in App.js, and every id must be unique.
const guestConnection = await register({
id: extensionId, // top-level id, non-empty
metadata, // from app-metadata.json (generated by a build hook)
methods: {
id: extensionId, // ⚠️ REQUIRED here too — the menu item won't render without it (see Gotchas)
mainMenu: {
getItems() { return [{ id, url: '/index.html#/route', label, icon }] }
},
secondaryNav: { // left panel, per object type
PROJECT: { getItems() { return [{ id, label, icon, url: '/route' }] } },
// register each separately: PROJECT, TASK, ISSUE, PORTFOLIO, PROGRAM
},
widgets: { // embed in a custom-form field
getItems() {
return [{
id, url: '/index.html#/widgets1', label,
dimensions: { height, width, maxHeight, maxWidth } // all optional
}]
}
},
}
})
Widget id/url/label are required; dimensions is optional.
Add a <Route> per extension-point url:
<Route exact path="custom-application" element={<CustomApplication />} />
sharedContext is get-only (.get(key) — not iterable). Confirmed shape from the Workfront host:
const ctx = conn?.sharedContext
const auth = ctx?.get('auth') // { imsClientId, imsOrgID, imsToken }
const user = ctx?.get('user') // { ID, email }
const host = ctx?.get('hostname') // e.g. ai-dev-arm.devtest.workfront-dev.adobe.com (no protocol)
// also: protocol; plus objCode, objID, isLoginAs, isInBulkEditing on object-scoped points
const imsToken = auth?.imsToken
const imsOrgId = auth?.imsOrgID // ⚠️ key is `imsOrgID` (capital ID) — NOT imsOrgId / imsOrg
Everything an action needs is right here — pass imsToken, imsOrgId (auth.imsOrgID), and host into actionWebInvoke. Don't call Workfront for the org: it's in auth, and a cross-origin currentUser fetch from the SPA is CORS-blocked anyway. Only set the x-gw-ims-org-id header when you have a value — Fetch turns undefined into the string "undefined" (→ 401 Org Id undefined). Widgets receive the same context.
Do heavy CSV/XLSX (spreadsheet) work in Web Workers — background threads, so the screen doesn't freeze — but never call the WF API inside a worker. Show a toast (small popup notice) on failure; never expose tokens.
id, and the fix is trivial. The WF template scaffolds register({ metadata, methods: { id: extensionId, mainMenu } }) with extensionId = '' in Constants.js. Two things are required for the item to appear: (1) give extensionId a non-empty value in Constants.js, and (2) keep id: extensionId under methods — that placement is exactly what the menu needs; if you remove it, the item still registers (Workfront even calls getItems) but silently never renders. Simplest working form: set extensionId, and keep id: extensionId inside methods (having it at the top level of the config too is fine). Don't be thrown off by reading @adobe/uix-guest: register(config) does new GuestServer(config.id) then guest.register(config.methods, …) and just forwards methods to the host — so from the guest source a bare methods.id looks like an ignored no-op. It isn't: Workfront's host side consumes methods.id, and that behavior isn't visible in the guest package. Verified in a live Main Menu extension — the item does not render without it. (This is easy to misdiagnose as an environment problem, or as dead code — it's neither.)aio app init -y (or skipping the "Add a custom button to Main Menu Item" prompt) generates no menu item at all — no mainMenu block, no view component, no App.js route, no icons.js, empty extensionId. Either answer that prompt during init, or hand-add: icons.js (exporting icon1/icon2), the mainMenu block, a <Route>, and the view component.web-src/src/utils.js exports actionWebInvoke(url, headers, params, options = { method: 'POST' }). Action URLs are injected into web-src/src/config.json at build time under both "<action>" and "<package>/<action>" — import actions from '../config.json' and look up by name; never hardcode a Runtime URL.register vs attach: the background registration frame uses register() (a GuestServer, declares the extension points); a displayed view uses attach() (a GuestUI). Both expose sharedContext.…/workfront/custom-applications/<extensionId>/<menuRoute> (org- and instance-scoped); that is the app's shareable URL, and after aio app deploy you should hand it to the user (build recipe in workfront-local-testing). First segment = the registration id; second = the menu item's #/route. Two traps: reusing the app id as the second segment (…/<extensionId>/<extensionId>) loads the background registration frame, not the view; and the raw CDN …/index.html#/route renders with no host → no sharedContext. If the item registers but won't render live, suspect the Workfront environment, not the code (see workfront-local-testing).TableView's "select all" header checkbox gives you the string 'all', not a Set. onSelectionChange for selectionMode="multiple" normally hands you a Set of row keys, and code like selectedKeys.size > 0 works fine when the user checks rows one at a time. But clicking the header's own "select all" checkbox sets selectedKeys to the literal string 'all' instead — 'all'.size is undefined, so a naive selectedKeys.size > 0 check silently evaluates to false and a bulk-action button stays disabled with no visible error. Always branch on the string case: const count = selectedKeys === 'all' ? totalItems : selectedKeys.size, and use the same branch anywhere you turn the selection into an array of ids (selectedKeys === 'all' ? items.map(i => i.id) : Array.from(selectedKeys)). This is easy to miss in testing if you only ever click individual row checkboxes.