界面已本地化;技能介绍与说明文档按所选语言由 AI 自动翻译,代码、命令、路径和专业术语保留原文,以原作者文档为准。
探索技能
开发与工程 · SKILL.md

workfront-ui-extension

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 快照 · 自动分类 · 以原作者文档为准

Aadobe / skillsApache-2.0

技能说明

GitHub

name: workfront-ui-extension description: "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 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.0

Workfront UI extension (front end)

Part 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 in assets/ExtensionRegistration.example.js.

Rules

  • One register() call wires everything up; individual views use attach().
  • Auth comes from sharedContext + getWFInstanceUrl() — Workfront supplies the signed-in user and instance; don't build a login.
  • Talk to the back end with actionWebInvoke only — the browser must never call Workfront directly (/attask/api/…). Back-end code lives in workfront-actions.
  • Every action replies with { data, error } — always check error before using data.

Extension points (in ExtensionRegistration)

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.

Routing (App.js)

Add a <Route> per extension-point url:

<Route exact path="custom-application" element={<CustomApplication />} />

Shared context

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.

Workers / errors

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.

Gotchas (from building a real Main Menu extension)

  • Main Menu item not rendering? It's the 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.
  • Calling actions: the template's 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.
  • The deployed app's URL is the Experience Cloud shell link — not the bare CDN. Clicking the Main Menu button navigates to …/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).
  • React Spectrum 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.
我的收藏