From 45550fc8db502962ad0e2aadf6012cf3004d8a6c Mon Sep 17 00:00:00 2001 From: Naradasi Tejaswi Date: Thu, 9 Jul 2026 16:08:04 +0530 Subject: [PATCH 1/6] Content Hub extensibility Skill --- .../appbuilder-action-scaffolder/README.md | 6 +- .../appbuilder-action-scaffolder/SKILL.md | 4 +- .../assets/contenthub-action-template.js | 71 + .../skills/appbuilder-cicd-pipeline/SKILL.md | 7 +- .../references/contenthub-deploy.md | 142 ++ .../skills/appbuilder-e2e-testing/SKILL.md | 10 +- .../skills/appbuilder-project-init/SKILL.md | 19 +- .../references/contenthub-scaffolding.md | 331 +++++ .../references/contenthub-templates.md | 1182 +++++++++++++++++ .../skills/appbuilder-testing/SKILL.md | 16 +- .../skills/appbuilder-ui-scaffolder/SKILL.md | 33 +- .../references/aem-extensions.md | 275 +++- 12 files changed, 2065 insertions(+), 31 deletions(-) create mode 100644 plugins/app-builder/skills/appbuilder-action-scaffolder/assets/contenthub-action-template.js create mode 100644 plugins/app-builder/skills/appbuilder-cicd-pipeline/references/contenthub-deploy.md create mode 100644 plugins/app-builder/skills/appbuilder-project-init/references/contenthub-scaffolding.md create mode 100644 plugins/app-builder/skills/appbuilder-project-init/references/contenthub-templates.md diff --git a/plugins/app-builder/skills/appbuilder-action-scaffolder/README.md b/plugins/app-builder/skills/appbuilder-action-scaffolder/README.md index ccf7d9ac..c5d57c9b 100644 --- a/plugins/app-builder/skills/appbuilder-action-scaffolder/README.md +++ b/plugins/app-builder/skills/appbuilder-action-scaffolder/README.md @@ -21,7 +21,7 @@ appbuilder-action-scaffolder/ │ ├── implementation-template.md ← Structured delivery artifact template │ ├── runtime-reference.md ← Action structure, params, response formats, SDKs, CLI │ └── action-patterns.md ← 12 complete action patterns with manifest + code -├── assets/ ← 9 JavaScript code templates +├── assets/ ← 10 JavaScript code templates │ ├── action-scaffold-template.js ← Minimal scaffold for quick prototyping │ ├── action-boilerplate.js ← Production-ready with logging and error handling │ ├── database-action-template.js ← Database CRUD with @adobe/aio-lib-db @@ -30,7 +30,8 @@ appbuilder-action-scaffolder/ │ ├── journaling-consumer-template.js ← Scheduled journal poller │ ├── large-payload-template.js ← Files SDK redirect for oversized responses │ ├── action-sequence-template.js ← Linear action sequence pipeline -│ └── asset-compute-worker-template.js ← AEM rendition processing worker +│ ├── asset-compute-worker-template.js ← AEM rendition processing worker +│ └── contenthub-action-template.js ← Content Hub web action (asset IDs → AEM Assets Author API → panel) └── evals/ └── evals.json ← 11 evaluation test cases ``` @@ -75,6 +76,7 @@ The skill follows a 6-step workflow (detailed in `references/playbook.md`): | Response exceeds 1 MB | large-payload-template.js | | Multi-action linear pipeline | action-sequence-template.js | | AEM rendition processing | asset-compute-worker-template.js | +| Content Hub web action (asset IDs → AEM Assets Author API → panel) | contenthub-action-template.js | ### Validate manifest before deploy diff --git a/plugins/app-builder/skills/appbuilder-action-scaffolder/SKILL.md b/plugins/app-builder/skills/appbuilder-action-scaffolder/SKILL.md index e440c715..1dcb29be 100644 --- a/plugins/app-builder/skills/appbuilder-action-scaffolder/SKILL.md +++ b/plugins/app-builder/skills/appbuilder-action-scaffolder/SKILL.md @@ -1,6 +1,6 @@ --- name: appbuilder-action-scaffolder -description: Create, implement, deploy, and debug Adobe Runtime actions with consistent layout, validation, and error handling. Use this skill whenever the user needs to add actions to an App Builder project, understand action structure (params, response format, web/raw actions), configure actions in the manifest, use App Builder SDKs (State, Files, Events, database), deploy and invoke actions via CLI, debug action issues, or implement patterns such as webhook receivers, custom event providers, journaling consumers, large payload redirects, action sequence pipelines, and Asset Compute workers. Also trigger when users mention serverless functions in Adobe context, action logging, IMS authentication for actions, or cron-style scheduled actions. +description: Create, implement, deploy, and debug Adobe Runtime actions with consistent layout, validation, and error handling. Use this skill whenever the user needs to add actions to an App Builder project, understand action structure (params, response format, web/raw actions), configure actions in the manifest, use App Builder SDKs (State, Files, Events, database), deploy and invoke actions via CLI, debug action issues, or implement patterns such as webhook receivers, custom event providers, journaling consumers, large payload redirects, action sequence pipelines, Asset Compute workers, and Content Hub web actions (the actions/generic/index.js that receives asset IDs from panels or modals, calls the AEM Assets Author API, and returns data to the Content Hub UI). Also trigger when users mention serverless functions in Adobe context, action logging, IMS authentication for actions, cron-style scheduled actions, or a Content Hub web action calling the AEM API. metadata: category: action-lifecycle license: Apache-2.0 @@ -26,6 +26,7 @@ Pick the template that matches the user's intent. Default to `assets/action-boil | Large payload response (>1 MB) | assets/large-payload-template.js | | Action sequence pipeline | assets/action-sequence-template.js | | Asset Compute worker (AEM renditions) | assets/asset-compute-worker-template.js | +| Content Hub web action (asset IDs → AEM Assets Author API → panel) | assets/contenthub-action-template.js | | Debug Runtime action issues | references/debugging.md | ## Fast Path (for clear requests) @@ -138,6 +139,7 @@ If there is any ambiguity — multiple patterns could fit, constraints are uncle - `assets/large-payload-template.js` — large payload response starter that writes oversized responses to Files storage and returns a redirect URL. - `assets/action-sequence-template.js` — Starter manifest and action layout for a linear action sequence pipeline. - `assets/asset-compute-worker-template.js` — Asset Compute worker scaffold for AEM rendition processing with the Asset Compute SDK. +- `assets/contenthub-action-template.js` — Content Hub web action starter that receives asset ID(s) plus auth context from a Content Hub panel/card/bulk action, calls the AEM Assets Author API server-side, and returns the result to the panel. ## Agent Integration diff --git a/plugins/app-builder/skills/appbuilder-action-scaffolder/assets/contenthub-action-template.js b/plugins/app-builder/skills/appbuilder-action-scaffolder/assets/contenthub-action-template.js new file mode 100644 index 00000000..a823dd68 --- /dev/null +++ b/plugins/app-builder/skills/appbuilder-action-scaffolder/assets/contenthub-action-template.js @@ -0,0 +1,71 @@ +const { Core } = require('@adobe/aio-sdk'); + +/** + * Content Hub web action. + * + * Data flow: a Content Hub panel / card action / bulk action passes the selected + * asset ID(s) plus auth context to this action, which calls the AEM Assets Author + * API server-side (CORS blocks the browser from calling AEM directly) and returns + * the result to the panel to render. + * + * Params passed from the extension (see contenthub-extensions.md): + * assetId — asset URN from host.assetDetails.getCurrentAsset() (or assetIds[] for bulk) + * aemHost — AEM author host from host.discovery.getAemHost() + * apiKey — from host.auth.getApiKey() (never hardcode) + * imsOrg — from host.auth.getIMSInfo() + * + * For authenticated AEM API calls, set require-adobe-auth: true in ext.config.yaml, + * send the Authorization header from the panel, and read the token below. + */ +async function main(params) { + const logger = Core.Logger('main', { level: params.LOG_LEVEL || 'info' }); + + try { + logger.info('Content Hub action invoked', JSON.stringify({ assetId: params.assetId })); + + // Input validation + const requiredParams = ['assetId', 'aemHost']; + const missingParams = requiredParams.filter(p => !params[p]); + if (missingParams.length > 0) { + return { + statusCode: 400, + body: { error: `Missing required parameters: ${missingParams.join(', ')}` } + }; + } + + const { assetId, aemHost, apiKey, imsOrg } = params; + // When require-adobe-auth: true, the gateway injects the bearer token here: + const token = params.__ow_headers?.authorization?.substring(7); + + const response = await fetch(`https://${aemHost}/adobe/assets/${assetId}/metadata`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'X-Api-Key': apiKey, // always from the frontend — never hardcode + 'x-gw-ims-org-id': imsOrg, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`AEM Assets Author API failed (${response.status})`); + } + + const data = await response.json(); + const metadata = data.value ?? data; + + logger.info('Content Hub action completed successfully'); + return { + statusCode: 200, + headers: { 'Content-Type': 'application/json' }, + body: { metadata } + }; + } catch (error) { + logger.error('Content Hub action failed:', error.message); + return { + statusCode: 500, + body: { error: error.message } + }; + } +} + +exports.main = main; diff --git a/plugins/app-builder/skills/appbuilder-cicd-pipeline/SKILL.md b/plugins/app-builder/skills/appbuilder-cicd-pipeline/SKILL.md index 049e00e5..fb42221b 100644 --- a/plugins/app-builder/skills/appbuilder-cicd-pipeline/SKILL.md +++ b/plugins/app-builder/skills/appbuilder-cicd-pipeline/SKILL.md @@ -8,8 +8,9 @@ description: >- user mentions CI/CD for App Builder, GitHub Actions for aio deploy, automated deployment pipelines, continuous integration, continuous delivery, deploy automation, multi-environment promotion, aio app add ci, or wants to automate their App Builder build and release process. - Also trigger when users mention deploy workflows, release pipelines, or GitHub secrets for - App Builder. + Also trigger when users mention deploy workflows, release pipelines, GitHub secrets for + App Builder, deploy a Content Hub extension, aio app deploy for AEM extensions, Extension + Manager approval, or automate deployment of a Content Hub or AEM UI extension. metadata: category: deployment-automation license: Apache-2.0 @@ -31,6 +32,7 @@ Set up CI/CD pipelines for Adobe App Builder projects — GitHub Actions (primar | Azure DevOps / GitLab CI / Jenkins | references/generic-pipeline-guide.md | | Secrets setup guide | references/secrets-management.md | | Debugging deploy failures | references/debugging.md | +| Content Hub extension deploy (manual `aio app deploy`, Stage→Prod, CDN URL, Extension Manager approval) | references/contenthub-deploy.md | ## Fast Path (for clear requests) @@ -93,6 +95,7 @@ If user specifies Azure DevOps, GitLab CI, or Jenkins → use `references/generi - Use `references/secrets-management.md` for OAuth S2S credential extraction and GitHub secrets setup. - Use `references/debugging.md` for troubleshooting deploy failures, CI errors, and workspace promotion issues. - Use `references/checklist.md` for pre-merge CI readiness validation. +- Use `references/contenthub-deploy.md` for manual Content Hub extension deployment (Stage → Production, CDN URL parsing, Extension Manager approval, re-deploy after code changes). - Use `assets/deploy-stage.yml`, `assets/deploy-prod.yml`, `assets/pr-test.yml` as workflow templates. - Use `assets/fetch-secrets.sh` to extract secret values from workspace configuration. - Official Adobe docs: [https://developer.adobe.com/app-builder/docs/guides/app_builder_guides/deployment/cicd-using-github-actions](https://developer.adobe.com/app-builder/docs/guides/app_builder_guides/deployment/cicd-using-github-actions) diff --git a/plugins/app-builder/skills/appbuilder-cicd-pipeline/references/contenthub-deploy.md b/plugins/app-builder/skills/appbuilder-cicd-pipeline/references/contenthub-deploy.md new file mode 100644 index 00000000..7e86adb6 --- /dev/null +++ b/plugins/app-builder/skills/appbuilder-cicd-pipeline/references/contenthub-deploy.md @@ -0,0 +1,142 @@ +# Adobe App Builder Extension Deployment + +Complete deployment workflow for a Content Hub extension (`aem/assets/contenthub/1`) from Stage through Production approval. + +--- + +## Prerequisites + +Before deploying: +1. `aio app run` has succeeded locally (extension visible with `ext=` URL param) +2. `allowedRepos` in `ExtensionRegistration.js` is populated with delivery repo IDs +3. The correct org and project are selected (`aio where` shows the right state) + +--- + +## Stage Deployment + +Stage is used for QA and stakeholder review before Production. + +```bash +# Switch workspace — unset CI/AIO_CLI_NO_TTY in subshell so credentials are downloaded +bash -c 'unset CI AIO_CLI_NO_TTY TERM; printf "y\ny\ny\ny\n" | aio app use -w Stage --overwrite' 2>/dev/null + +# Deploy — capture output +aio app deploy 2>&1 | tee /tmp/aio-deploy.log +``` + +After deploy completes, **always** parse the CDN URL from the log — even if actions failed: + +```bash +CDN_URL=$(grep -Eo 'https://[^ ]+adobeio-static\.net[^ ]*' /tmp/aio-deploy.log | grep 'index\.html' | tail -1) +# If no index.html variant found, take any adobeio-static.net URL: +CDN_URL=${CDN_URL:-$(grep -Eo 'https://[^ ]+adobeio-static\.net[^ ]*' /tmp/aio-deploy.log | tail -1)} +``` + +Open Content Hub automatically with the deployed URL — **do not just print it**. Replace the local `ext=https://localhost:9080` with `ext=/index.html` and keep the Content Hub deep-link hash: + +```bash +open "https://experience.adobe.com/?devMode=true&ext=${CDN_URL}/index.html#/assets/contenthub/" +``` + +**Partial failure rule:** If web assets deployed but actions failed (Runtime not provisioned in the org, or 401 on action deploy), still open the CDN URL. The extension UI loads correctly. Actions will fail only when called. Do not block on action deployment errors. + +--- + +## Production Deployment + +```bash +# Switch to Production workspace — unset CI/AIO_CLI_NO_TTY in subshell +bash -c 'unset CI AIO_CLI_NO_TTY TERM; printf "y\ny\ny\ny\n" | aio app use -w Production --overwrite' 2>/dev/null + +# Deploy to Production +aio app deploy +``` + +Production deployment must be followed by an approval step before the extension is visible to all users in the org. + +--- + +## Extension Manager Approval + +1. Open `https://experience.adobe.com/aem/extension-manager` +2. Find your extension by name +3. Click **Approve** +4. The extension becomes visible to all users in your org — no `ext=` URL parameter needed + +--- + +## Workspace Selection (`aio app use`) + +`aio app use` rewrites `.aio` and `.env` to point at the specified workspace. Always run it before `aio app deploy` when switching targets. + +```bash +# Check current workspace +CI=true AIO_CLI_NO_TTY=true NO_COLOR=1 aio where + +# Switch workspace — unset CI/AIO_CLI_NO_TTY in subshell so credentials are downloaded +bash -c 'unset CI AIO_CLI_NO_TTY TERM; printf "y\ny\ny\ny\n" | aio app use -w Stage --overwrite' 2>/dev/null +bash -c 'unset CI AIO_CLI_NO_TTY TERM; printf "y\ny\ny\ny\n" | aio app use -w Production --overwrite' 2>/dev/null +``` + +If `aio app use -w ` does not find the workspace by name, use explicit flags (also in clean subshell): +```bash +bash -c 'unset CI AIO_CLI_NO_TTY TERM; aio app use --org --project --workspace Stage --no-input' +``` + +--- + +## Re-deploy After Code Changes + +```bash +# Rebuild and redeploy (from the project directory) +aio app deploy +``` + +For UI-only changes (no action changes), `aio app deploy` is still the right command — it rebuilds the web-src bundle and pushes to CDN. + +--- + +## Troubleshooting Deployments + +### Extension visible with `ext=` but not after approval + +The extension was approved in the wrong workspace. The `ext=` URL param bypasses workspace checks. + +```bash +aio where # confirm you're on Production +aio app use -w Production +aio app deploy +# Then re-approve in Extension Manager +``` + +### Extension invisible to some users + +The App Builder project includes extra Adobe services (AEM Assets Author API, Cloud Manager, etc.). Only users entitled to those services see the extension. + +**Fix:** Remove all non-required services from the App Builder project in Adobe Developer Console. Keep only Runtime. Redeploy and reapprove. + +### `aio app deploy` fails with auth error + +`.env` is missing or stale. Re-wire the workspace (unset CI in a subshell so credentials are downloaded): +```bash +bash -c 'unset CI AIO_CLI_NO_TTY TERM; printf "y\ny\ny\ny\n" | aio app use -w Stage --overwrite' 2>/dev/null +aio app deploy +``` + +### CDN propagation delay + +After `aio app deploy` succeeds, CDN propagation can take 1-2 minutes. If the extension shows the old version immediately after deploy, wait a moment and hard-refresh. + +--- + +## Full Deployment Checklist + +- [ ] `aio where` shows the correct org, project, and workspace +- [ ] `aio app use -w ` run in a clean subshell (`unset CI AIO_CLI_NO_TTY TERM`) before deploying +- [ ] `aio app deploy` completed without errors (partial web-only success is still usable) +- [ ] Tested with the deployed CDN URL (not localhost) +- [ ] **Content Hub / AEM surfaces** — deployed URL opened with `?devMode=true&ext=/index.html` +- [ ] **Content Hub only** — `allowedRepos` populated with target delivery repo IDs before Production deploy +- [ ] For Production: approved in Extension Manager +- [ ] For Production: verified without `ext=` URL parameter diff --git a/plugins/app-builder/skills/appbuilder-e2e-testing/SKILL.md b/plugins/app-builder/skills/appbuilder-e2e-testing/SKILL.md index abbd9f0d..98f8004b 100644 --- a/plugins/app-builder/skills/appbuilder-e2e-testing/SKILL.md +++ b/plugins/app-builder/skills/appbuilder-e2e-testing/SKILL.md @@ -2,10 +2,11 @@ name: appbuilder-e2e-testing description: >- Use this skill whenever the user wants browser-based end-to-end tests for an Adobe App Builder - application. Covers Playwright E2E testing for ExC Shell SPAs, AEM extension UIs, and full-stack - flows. Use when the user mentions: "E2E test", "end-to-end test", "Playwright", "browser test", - "test my SPA in the browser", "test my AEM extension", "test the full flow", "integration test - with UI", "headless browser test", "E2E in CI". + application. Covers Playwright E2E testing for ExC Shell SPAs, AEM extension UIs (Content Fragment + Console, Universal Editor, Content Hub), and full-stack flows. Use when the user mentions: "E2E + test", "end-to-end test", "Playwright", "browser test", "test my SPA in the browser", "test my AEM + extension", "test my Content Hub extension", "test a Content Hub panel, card action, or bulk action", "test the + full flow", "integration test with UI", "headless browser test", "E2E in CI". This skill is for BROWSER-based testing only. For Jest unit tests of actions or React components, use appbuilder-testing instead. metadata: @@ -27,6 +28,7 @@ Identify the user's intent, then read the referenced sections to generate tailor | E2E test for ExC Shell SPA | `references/e2e-testing-patterns.md` | `assets/playwright.config.ts`, `assets/e2e-test-template.spec.ts` | | Test AEM extension in browser | `references/aem-extension-testing.md` | `assets/playwright.config.ts` | | E2E tests in CI pipeline | `references/ci-integration.md` | `assets/e2e-ci-workflow.yml` | +| Content Hub extension E2E (panel render, card action, bulk action) | `references/aem-extension-testing.md` | `assets/playwright.config.ts` | ## Fast Path (for clear requests) diff --git a/plugins/app-builder/skills/appbuilder-project-init/SKILL.md b/plugins/app-builder/skills/appbuilder-project-init/SKILL.md index eec1c38a..39b3d24c 100644 --- a/plugins/app-builder/skills/appbuilder-project-init/SKILL.md +++ b/plugins/app-builder/skills/appbuilder-project-init/SKILL.md @@ -1,11 +1,11 @@ --- name: appbuilder-project-init -description: Initialize an Adobe App Builder project end-to-end without Developer Console UI clicks. Creates the Console project and workspace, subscribes APIs (including those needing a product profile), maps user intent to the right template, runs non-interactive `aio app init`, and guides post-init customization. Use whenever the user mentions creating an App Builder app, scaffolding a project, `aio app init`, setting up an Experience Cloud extension, adding actions or web assets, creating a Console project or workspace, adding APIs, or bootstrapping App Builder — even if they don't say "App Builder". Also for SPA templates, AEM extensions, API Mesh, Asset Compute workers, and MCP server projects. Also handles debugging init failures — template not found, `aio app init` hangs or times out, Node version mismatches, npm install failures, post-init build errors, `aio login` issues, `aio app run` showing nothing, or `aio console project create` / `workspace create` / `workspace api add` errors. +description: Initialize or scaffold an Adobe App Builder project end-to-end without Developer Console UI clicks. Creates the Console project and workspace, subscribes APIs, maps user intent to the right template, and runs non-interactive `aio app init`. Use whenever the user mentions creating an App Builder app, scaffolding a project, `aio app init`, setting up an Experience Cloud extension, adding actions or APIs, or bootstrapping App Builder — even if they don't say "App Builder". Also for SPA templates, AEM extensions, API Mesh, Asset Compute workers, and MCP projects. Also scaffolds a Content Hub extension (`aem/assets/contenthub/1`) from scratch — Console setup, file generation, build, dev server, deploy — when the user says "scaffold a Content Hub extension" or names a Content Hub surface (asset details panel, card action, bulk action, add assets wizard). Also handles debugging init failures — `aio app init` hangs, Node mismatches, npm failures, `aio login` issues, or `aio console` project/workspace/API errors. metadata: category: project-initialization license: Apache-2.0 compatibility: Requires aio CLI (Adobe I/O CLI) — install or refresh with `npm install -g @adobe/aio-cli` so the bundled plugins (`aio-cli-plugin-console`, `aio-cli-plugin-app`, etc.) are current. Node.js 18+ (Node 24 supported on Stage runtimes). Bash shell. -allowed-tools: Bash(aio:*) Bash(npm:*) Bash(node:*) Read Write +allowed-tools: Bash(aio:*) Bash(npm:*) Bash(node:*) Bash(mkdir:*) Bash(lsof:*) Bash(kill:*) Bash(open:*) Read Write --- # App Builder Project Initialization @@ -143,6 +143,7 @@ Pick the template that matches the user's intent. When unclear, default to `@ado | User wants | Template | | --- | --- | | SPA with actions + React UI | @adobe/generator-app-excshell | +| Content Hub extension (asset details panel, card action, bulk action, add assets wizard) | Content Hub Scaffolding — see section below | | AEM Content Fragment Console extension | @adobe/aem-cf-admin-ui-ext-tpl | | AEM React SPA (WKND-based) | @adobe/generator-app-aem-react | | Adobe API Mesh (GraphQL) | @adobe/generator-app-api-mesh | @@ -152,6 +153,14 @@ Pick the template that matches the user's intent. When unclear, default to `@ado For a headless/backend-only request, prefer `init-bare` when possible. If the user still needs a template that generates UI files, plan a post-init cleanup so the final project has no `web-src` frontend directory or web manifest wiring. +## Content Hub Extension Scaffolding + +For a Content Hub extension (`aem/assets/contenthub/1` — asset details panels, card actions, bulk actions, add assets wizard) this skill runs a dedicated end-to-end workflow instead of a generator template: name → namespace selection → Console setup → file generation → npm install → `aio app use` → build → dev server → cert acceptance → open in Content Hub → deploy. + +Read [`references/contenthub-scaffolding.md`](references/contenthub-scaffolding.md) for the full step-by-step workflow and [`references/contenthub-templates.md`](references/contenthub-templates.md) for the scaffold file templates. The workflow's final step does a manual first `aio app deploy`; for the full deployment story (Stage → Production, Extension Manager approval, CI/CD) chain to the `appbuilder-cicd-pipeline` skill. + +**When it triggers:** the user says "create/scaffold a Content Hub extension", or names a Content Hub surface ("asset details panel", "card action button", "bulk action", "add assets wizard"). Once the extension is scaffolded and running, chain to `appbuilder-ui-scaffolder` for UI customization (React Spectrum patterns for each namespace). + ## Initialize via Script The `aio app *` wrappers go through a single script: `scripts/init.sh`. (Console bootstrap commands are called directly — see the **Bootstrap** section above for the rationale.) @@ -303,4 +312,8 @@ After initialization, hand off to: - [references/bootstrap.md](references/bootstrap.md) — Agentic Developer Console bootstrap (project, workspace, API subscriptions) via raw `aio console …` commands from the latest `@adobe/aio-cli` - [references/templates.md](references/templates.md) — Template catalog with intent mapping and per-template post-init guidance -- [references/debugging.md](references/debugging.md) — Troubleshooting guide for init failures, Node/npm issues, login problems, and first-run errors \ No newline at end of file +- [references/debugging.md](references/debugging.md) — Troubleshooting guide for init failures, Node/npm issues, login problems, and first-run errors +- [references/contenthub-scaffolding.md](references/contenthub-scaffolding.md) — Full Content Hub extension scaffolding workflow (Steps 1–17: Console setup, namespace selection, file generation, build, dev server, cert acceptance, deploy) +- [references/contenthub-templates.md](references/contenthub-templates.md) — All Content Hub scaffold file templates (`app.config.yaml`, `ext.config.yaml`, `ExtensionRegistration.js`, `App.js`, per-namespace components, `actions/generic/index.js`) + +For deploying a Content Hub extension (Stage → Production, CDN URL, Extension Manager approval, CI/CD), use the `appbuilder-cicd-pipeline` skill — see its `references/contenthub-deploy.md`. \ No newline at end of file diff --git a/plugins/app-builder/skills/appbuilder-project-init/references/contenthub-scaffolding.md b/plugins/app-builder/skills/appbuilder-project-init/references/contenthub-scaffolding.md new file mode 100644 index 00000000..86801818 --- /dev/null +++ b/plugins/app-builder/skills/appbuilder-project-init/references/contenthub-scaffolding.md @@ -0,0 +1,331 @@ +# Content Hub Extension Scaffolding — Full Workflow + +Complete step-by-step workflow for scaffolding a **Content Hub** App Builder extension (`aem/assets/contenthub/1`) from scratch: Console setup, namespace selection, file generation, npm install, build, dev server, cert acceptance, and deploy. + +Read this file when `appbuilder-project-init` is asked to create/scaffold a Content Hub extension. + +> **When to use this:** the user wants to create a NEW Content Hub extension — tab panels in Asset Details, card/collection-tile action buttons, bulk-action-bar buttons, or Add Assets wizard panels. This drives the whole lifecycle from one prompt to a running, open browser tab. If the user already has a scaffolded project and only wants to customize the UI, use `appbuilder-ui-scaffolder` (§ Content Hub in `references/aem-extensions.md`) instead. + +**Fixed facts for this surface** (used by every step below): + +| | Value | +| --- | --- | +| Extension point | `aem/assets/contenthub/1` | +| Source dir | `src/aem-assets-contenthub-1/` | +| SDK | `@adobe/uix-guest` | +| Templates | [`references/contenthub-templates.md`](contenthub-templates.md) | +| Namespaces | `assetDetails` (tab panels), `card` (asset-card + collection-tile buttons), `selectionBar` (bulk-action bar), `addAssets` (wizard panels + `beforeUpload`/`onUploadComplete` hooks) | +| Local dev open URL | `https://experience.adobe.com/?devMode=true&ext=https://localhost:9080#/assets/contenthub/` | + +Use `AskUserQuestion` for every user decision. Never print a test URL without opening it. Never ask the user to type anything (except when they choose "Other"). + +--- + +## Interaction Rules (how to ask, decline, and resume) + +**1. Always use `AskUserQuestion` for choices.** Never print a numbered list and ask the user to type a number. The only typing the user does is selecting "Other". + +**2. Every option states what it does — never bare "Yes"/"No".** Give each option a `label` naming the action and a `description` spelling out the consequence (e.g. `label: "Yes — install the aio CLI now"`, `description: "Runs npm install -g @adobe/aio-cli, ~30s"`). The decline option must say what happens instead. + +**3. Declining must never dead-end.** When the user declines, reply with (a) a one-line confirmation of what was skipped, (b) the exact command/action to run, and (c) how to resume — tell them to type **`continue`** and which step you'll pick up from. + +**4. ALL file writes are fully automatic — no confirmation, ever.** Every `Write`/`Edit`/`Bash mkdir` in this flow is silent and automatic. Do NOT ask "Ready to scaffold?", "Should I create this file?", or announce each file. Just write them all. The only `AskUserQuestion` calls are genuine decisions (name, namespaces, workspace, output dir, install/login/deploy). + +**5. If interrupted mid-run, tell the user how to resume.** End with what completed, what's pending, and that typing **`continue`** resumes from the next step (state the step number). + +**6. When a Bash command is denied at the permission prompt, never silently halt.** Reply with which command was blocked, why it's needed, and that they can approve it (picking "Yes, and don't ask again" avoids future prompts for routine `mkdir`/`npm`/`aio` commands) or run it themselves and type **`continue`**. + +--- + +## Full Workflow + +### Step 1 — Extension Name + +Use `AskUserQuestion`. Suggest Content Hub-appropriate names: + +``` +question: "What should we name your extension?" +options: + - label: "asset-metadata-panel" + - label: "asset-card-actions" + - label: "asset-bulk-export" +``` + +The user can select "Other" to type their own kebab-case name. Store as `extensionName`. Validate kebab-case (lowercase letters + hyphens only); if invalid (e.g. `My Extension`), suggest the corrected form (`my-extension`) via another `AskUserQuestion`. + +### Step 2 — Choose Content Hub namespace(s) + +**Auto-select rule:** if the prompt already names a namespace, do NOT ask — auto-select and skip to Step 3: +- "asset card" / "card action" / "buttons on cards" → `["card"]` +- "asset details panel" / "tab panel in asset details" → `["assetDetails"]` +- "bulk action" / "selection bar" → `["selectionBar"]` +- "Add Assets wizard" / "before upload hook" / "hydration panel" → `["addAssets"]` +- "card and bulk actions" → `["card", "selectionBar"]` + +**Only ask when the prompt is generic** ("create a Content Hub extension" with no namespace hint). Use `AskUserQuestion` with `multiSelect: true`: + +``` +question: "Which Content Hub surfaces do you want to extend?" (multiSelect: true) +options: + - label: "Asset Details panel" + description: "Custom tab panels in the Asset Details Dialog side rail — assetDetails namespace" + - label: "Asset card / collection tile action" + description: "Buttons on asset card menus (Assets grid, inside a collection, link share) and on collection tiles — card namespace. onActionClick(resourceType, buttonId, resourceId, actionContext)." + - label: "Selection bar (bulk action)" + description: "Buttons in the bulk-action bar shown when assets are selected — selectionBar namespace. onActionClick(buttonId, assetIds[])." + - label: "Add Assets wizard" + description: "Panels before/after the Upload step, gate/enrich metadata (beforeUpload), react after upload (onUploadComplete) — addAssets namespace." +``` + +Store the selected keys as `namespaces` (at least one). Notes: `card`/`selectionBar` open a modal on click via `host.modal.openDialog()`, so `ExtensionRegistration.js` uses `let guestConnection` (not `const`). `addAssets` panels use `postMessage` for step readiness — never call `openDialog` from a wizard panel. Step 10 uses `namespaces` to decide which component files to write. + +### Step 3 — Workspace + +``` +question: "Which workspace should this extension use?" +options: + - label: "Stage" description: "For development and testing — recommended to start here" + - label: "Production" description: "For final release" +``` + +Store as `workspace`. + +### Step 4 — Output Directory + +``` +question: "Where should the project be created?" +options: + - label: "~/Desktop/" description: "Create on your Desktop (recommended)" + - label: "~/Documents/" description: "Create in your Documents folder" +``` + +Resolve `outputPath` to an absolute path (e.g. `$HOME/Desktop/`). + +### Step 5 — Check aio CLI Installed + +```bash +aio --version 2>/dev/null +``` + +- **Installed:** continue. +- **Not installed:** `AskUserQuestion` — "Yes — install it now" (`npm install -g @adobe/aio-cli`) or "No — I'll install it myself" (print the command, pause, resume on `continue`). + +### Step 6 — Check Login AND Validate the Token + +`aio where` alone is NOT sufficient — a token can look logged-in yet be rejected server-side with `401 ... ErrInvalidOauthToken`. This is the #1 cause of "401 on everything" — a **stale token**, not a restricted org. The only fix is `aio login --force`. + +**6a — Context check (fast, no browser):** +```bash +CI=true AIO_CLI_NO_TTY=true NO_COLOR=1 aio where 2>/dev/null +``` +If empty or "not logged": run login in a subshell with the flags **unset** (`env VAR=` is not enough — must `unset`): +```bash +bash -c 'unset CI AIO_CLI_NO_TTY TERM; aio login' +``` +The browser opens; wait up to 3 minutes for exit 0. Do NOT ask permission. + +**6b — Token-validity probe:** even if context looked fine, make one real Console API call: +```bash +CI=true AIO_CLI_NO_TTY=true NO_COLOR=1 aio console org list --json 2>&1 | head -c 400 +``` +- Returns a JSON array of orgs → token valid. Cache the org list for Step 7. +- Contains `401`/`Unauthorized`/`ErrInvalidOauthToken` → stale token. Refresh **without asking** (the user authorized the flow by running the skill): + ```bash + bash -c 'unset CI AIO_CLI_NO_TTY TERM; aio login --force' + ``` + Wait for exit 0, re-run the probe to confirm it returns orgs. + +**Critical:** never run `aio login`/`login --force` with `CI`, `AIO_CLI_NO_TTY`, or `TERM=dumb` set — those suppress the browser and login silently fails. + +### Step 7 — Resolve Org + +Use the cached org list from 6b (or re-run `aio console org list --json`). +- **One org:** auto-select `aio console org select `, store `activeOrgId`. +- **Multiple:** `AskUserQuestion` (label = org name, description = orgId; mark the current one). Then `aio console org select `. +- **Zero:** broken token — `aio login --force`, retry. + +### Step 8 — Resolve Project + +```bash +CI=true AIO_CLI_NO_TTY=true NO_COLOR=1 aio console project list --json 2>/dev/null +``` +- **Empty:** auto-create `aio console project create -n --json`, select it, `isNewProject = true`. +- **Projects exist:** always `AskUserQuestion` (one option per project + "Create a new project"). Never auto-select an existing project. On select: `aio console project select `. On create: ask for an alphanumeric name (no hyphens), create, select. +- If `create` errors "already exists": list, select the match. +- If `project list` returns 401 here: stale token mid-flow — `aio login --force`, retry (never fall back to `aio where` values). + +Store `activeProjectId`, `activeProjectName`, `isNewProject`. + +### Step 9 — Select Workspace + I/O Runtime note + +```bash +CI=true AIO_CLI_NO_TTY=true NO_COLOR=1 aio console workspace select +``` +**Do NOT run `aio console workspace service add`** — that subcommand does not exist in current aio CLI and isn't needed: every App Builder workspace gets an I/O Runtime namespace automatically, and `aio app use` (Step 12) downloads the creds into `.env`. The #1 reason `.env` comes back empty is a stale token, not a missing entitlement. + +If `isNewProject = false`, create the workspace if missing: +```bash +CI=true AIO_CLI_NO_TTY=true NO_COLOR=1 aio console workspace list --projectName --json 2>/dev/null +# if not present: +CI=true AIO_CLI_NO_TTY=true NO_COLOR=1 aio console workspace create --projectName --name --json 2>/dev/null +``` + +### Step 10 — Scaffold Project Files + +**FULLY AUTOMATIC — start writing immediately, zero questions.** Say "Scaffolding project files to ``..." then `mkdir` + `Write` every file in one uninterrupted sequence. + +```bash +mkdir -p /src/aem-assets-contenthub-1/web-src/src/components +mkdir -p /src/aem-assets-contenthub-1/actions/generic +mkdir -p /hooks +``` + +Read [`references/contenthub-templates.md`](contenthub-templates.md) and write all scaffold files, substituting `{{EXTENSION_NAME}}`, `{{DISPLAY_NAME}}`, `{{EXTENSION_DESCRIPTION}}`: + +- `package.json`, `app.config.yaml`, `src/aem-assets-contenthub-1/ext.config.yaml`, `extension-manifest.json`, `.eslintrc.js`, `hooks/post-deploy.js` +- `src/aem-assets-contenthub-1/web-src/index.html`, `web-src/src/index.js`, `index.css`, `config.json` +- `web-src/src/components/Constants.js`, `App.js`, `ExtensionRegistration.js` +- `src/aem-assets-contenthub-1/actions/utils.js`, `actions/generic/index.js` +- Namespace components — write **only** for namespaces selected in Step 2: `TabPanel.js` (assetDetails), `CardActionModal.js` (card), `SelectionBarModal.js` (selectionBar), `AddAssetsPanel.js` (addAssets) + +Keep `App.js` routes and the `ExtensionRegistration.js` `methods` object limited to the selected namespaces. + +### Step 11 — npm install + +```bash +cd +npm install +``` +If E401 (registry auth): `npm install --registry https://registry.npmjs.org`. + +### Step 12 — Wire to Workspace (`aio app use`) + +**Must run in a subshell with CI/AIO_CLI_NO_TTY unset**, or credential download is silently skipped and `.env` stays empty → 401 on build/deploy: +```bash +cd +bash -c 'unset CI AIO_CLI_NO_TTY TERM; aio app use -w --overwrite --no-input' +``` +If non-zero: +```bash +bash -c 'unset CI AIO_CLI_NO_TTY TERM; printf "y\ny\ny\ny\n" | aio app use -w --overwrite' +``` +Check `.env`: +```bash +node -e "const fs=require('fs');try{const c=fs.readFileSync('/.env','utf8');console.log(/AIO_RUNTIME_NAMESPACE=\S+/.test(c)&&/AIO_RUNTIME_AUTH=\S+/.test(c)?'VALID':'INVALID')}catch(e){console.log('INVALID')}" +``` +**If INVALID:** almost always a stale token — `aio login --force`, then re-run `aio app use`. Do NOT run `workspace service add`. Only if `.env` is still empty after a confirmed-fresh token is the workspace genuinely without Runtime (rare; UI still works, only actions unavailable). + +### Step 13 — Build + +```bash +cd +aio app build +``` +If a config validation error: `aio app build --no-config-validation`, fix the YAML, rebuild without the flag. + +### Step 14 — Start Dev Server + +Free port 9080 if taken (the user authorized the flow — don't ask): +```bash +lsof -ti:9080 # if a PID: kill 2>/dev/null; sleep 2 (kill -9 if still held) +``` +Start in background with `PORT=9080` explicitly, poll the log for the localhost URL: +```bash +cd +PORT=9080 aio app run > /tmp/aio-run-.log 2>&1 & +for i in $(seq 1 60); do grep -qE "https?://localhost:[0-9]+" /tmp/aio-run-.log 2>/dev/null && break; sleep 2; done +cat /tmp/aio-run-.log +``` +- URL appears → proceed to Step 15. +- 120s with no URL → `AskUserQuestion`: "Yes — rebuild and retry" (re-run build then the run loop) or "No — I'll start it manually" (print `cd && PORT=9080 aio app run`, resume on `continue` from Step 15). + +### Step 15 — Open Cert Page (after dev server URL confirmed) + +```bash +open "https://localhost:9080" # xdg-open on Linux, start "" on Windows +``` +Then `AskUserQuestion`: "Done — open the extension" (proceed to Step 16) or "Reopen the cert page" (re-`open` and ask again). The panel stays blank until the self-signed cert is accepted (Advanced → Proceed to localhost, or type `thisisunsafe`). + +### Step 16 — Open Content Hub Automatically + +Only after "Done" — open via Bash, **never just print the URL**: +```bash +open "https://experience.adobe.com/?devMode=true&ext=https://localhost:9080#/assets/contenthub/" +``` +Rules: **no `&repo=`** (the scaffold sets `allowedRepos = []`, so any repo works for local dev); **no `/index.html`** after `localhost:9080` (that's only for deployed CDN URLs). + +### Step 17 — Ask About Deployment + +The workspace was chosen in Step 3 — don't ask again. Just: +``` +question: "Extension is running locally on . Deploy it now?" +options: + - label: "Yes — deploy to now" description: "Runs aio app deploy, then opens the deployed CDN test URL automatically" + - label: "Not now — keep running locally" description: "Stays on localhost:9080; type 'deploy' later to deploy from Step 17" +``` +If "Yes": run the manual first-deploy now. In brief: +```bash +cd +aio app deploy 2>&1 | tee /tmp/aio-deploy-.log +``` +Parse the `*.adobeio-static.net` CDN base from the log (regardless of exit code) and **open** the deployed test URL — never just print it: +```bash +open "https://experience.adobe.com/?devMode=true&ext=/index.html#/assets/contenthub/" +``` +**Partial failure is still success:** if web assets deployed but actions failed (no Runtime / 401), the UI still works — open the CDN URL and note actions are unavailable; don't treat it as blocking. If `workspace` is Production, after opening offer `AskUserQuestion` to open Extension Manager (`https://experience.adobe.com/aem/extension-manager`) for approval. + +For the full deployment story — Stage → Production promotion, CDN URL parsing, Extension Manager approval, re-deploy after code changes, and automated CI/CD pipelines — hand off to the **`appbuilder-cicd-pipeline`** skill (see its `references/contenthub-deploy.md`). + +**After Step 17 (deployed or local), ALWAYS print the "Where to edit" map** — filter rows to the namespaces selected in Step 2: + +``` +## Where to edit your extension +All UI files are under `src/aem-assets-contenthub-1/web-src/src/components/` + +| What you want to change | File to edit | +|---|---| +| Which panels / buttons appear, their title, icon, or label | `ExtensionRegistration.js` | +| Asset Details panel content | `TabPanel.js` ← only if assetDetails selected | +| Card action modal content | `CardActionModal.js` ← only if card selected | +| Selection bar (bulk action) content | `SelectionBarModal.js` ← only if selectionBar selected | +| Add Assets wizard panel content | `AddAssetsPanel.js` ← only if addAssets selected | +| Server-side logic / AEM API calls | `actions/generic/index.js` | +``` + +Then: *"Let me know what you'd like to build and I'll make the changes."* (chains to `appbuilder-ui-scaffolder`). + +--- + +## Failure Recovery + +| Symptom | Action | +| --- | --- | +| `aio where` shows "not logged" | `bash -c 'unset CI AIO_CLI_NO_TTY TERM; aio login'` — don't ask, browser opens, wait for exit 0 | +| `aio console *` returns 401 / `ErrInvalidOauthToken` (even though `aio where` looks logged-in) | **Stale token, NOT a restricted org.** `aio login --force`, retry. Never fall back to `aio where` values. | +| `aio console project create` → "already exists" | List projects, select the match, continue | +| `aio console workspace create` → "already exists" | Skip, continue | +| `aio app use` exits non-zero | `bash -c 'unset CI AIO_CLI_NO_TTY TERM; aio app use -w --overwrite --no-input'` | +| `.env` missing `AIO_RUNTIME_NAMESPACE` | Usually a stale token — `aio login --force` then re-run `aio app use`. Do NOT run `workspace service add`. | +| `npm install` → E401 | `npm install --registry https://registry.npmjs.org` | +| `aio app build` → validation error | `aio app build --no-config-validation`; fix YAML; rebuild without flag | +| `aio app run` log never shows `localhost:9080` | Re-run `aio app build`; check `node -v` ≥ 18; read `/tmp/aio-run-.log` | +| Panel blank in browser | Cert not accepted — `open https://localhost:9080` and ask again | +| Content Hub panel not visible | Check `allowedRepos` is empty; URL uses `#/assets/contenthub/`; `devMode=true` present; card/selectionBar require the `EXTENSIBILITY_AEM_CONTENTHUB` flag | +| Deploy: CDN URL not found in log | `cat /tmp/aio-deploy-.log`; look for any `.adobeio-static.net` URL | + +--- + +## Quality Checklist + +- [ ] `app.config.yaml` uses `aem/assets/contenthub/1` +- [ ] Source directory is `src/aem-assets-contenthub-1/` +- [ ] `ExtensionRegistration.js` imports `register`, secondary pages import `attach`, both use the same `extensionId` from `Constants.js` +- [ ] `ExtensionRegistration.js` uses `let guestConnection` if `card` or `selectionBar` is selected +- [ ] Only the selected namespaces' components and routes exist +- [ ] `npm install` succeeded (Step 11) +- [ ] `aio app use` ran in a subshell with `unset CI AIO_CLI_NO_TTY TERM` (Step 12) +- [ ] `.env` has `AIO_RUNTIME_NAMESPACE` and `AIO_RUNTIME_AUTH` +- [ ] `aio app build` succeeded (Step 13) +- [ ] `PORT=9080 aio app run` is running and the log shows `localhost:9080` (Step 14) +- [ ] Cert page opened via `open` **after** the dev server URL was confirmed (Step 15) +- [ ] Content Hub opened via `open` (Bash), not just printed (Step 16) diff --git a/plugins/app-builder/skills/appbuilder-project-init/references/contenthub-templates.md b/plugins/app-builder/skills/appbuilder-project-init/references/contenthub-templates.md new file mode 100644 index 00000000..b170be47 --- /dev/null +++ b/plugins/app-builder/skills/appbuilder-project-init/references/contenthub-templates.md @@ -0,0 +1,1182 @@ +# Content Hub Extension File Templates + +Complete templates for every Content Hub extension scaffold file. Before writing, substitute: +- `{{EXTENSION_NAME}}` → kebab-case name (e.g. `my-asset-viewer`) +- `{{DISPLAY_NAME}}` → Title Case name (e.g. `My Asset Viewer`) +- `{{EXTENSION_DESCRIPTION}}` → one-sentence description + +Extension point used throughout: `aem/assets/contenthub/1` +Source directory: `src/aem-assets-contenthub-1/` + +> Only write the namespace components you need — `TabPanel.js` (assetDetails), `CardActionModal.js` (card), `SelectionBarModal.js` (selectionBar), `AddAssetsPanel.js` (addAssets). The shared skeleton (`package.json`, `index.html`, `index.js`, `index.css`, `Constants.js`, `App.js`, `actions/*`, `hooks/post-deploy.js`, `.eslintrc.js`) is always written. + + +--- + +## `package.json` + +```json +{ + "name": "{{EXTENSION_NAME}}", + "version": "1.0.0", + "description": "{{EXTENSION_DESCRIPTION}}", + "author": "", + "license": "Apache-2.0", + "scripts": { + "test": "jest --passWithNoTests --testPathIgnorePatterns web-src", + "dev": "aio app run", + "build": "aio app build", + "deploy": "aio app deploy", + "undeploy": "aio app undeploy" + }, + "dependencies": { + "@adobe/aio-sdk": "^5.0.0", + "@adobe/uix-guest": "0.10.5", + "@adobe/react-spectrum": "^3.0.0", + "chalk": "^4.0.0", + "js-yaml": "^4.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^6.0.0", + "react-error-boundary": "^4.0.0" + }, + "devDependencies": { + "@adobe/eslint-config-aio-lib-config": "^3.0.0", + "eslint": "^8.0.0", + "jest": "^29.0.0" + }, + "engines": { + "node": "^18 || ^20" + } +} +``` + +--- + +## `app.config.yaml` + +```yaml +extensions: + aem/assets/contenthub/1: + $include: src/aem-assets-contenthub-1/ext.config.yaml +``` + +**Critical (Content Hub):** Use `aem/assets/contenthub/1` — not the deprecated `aem/contenthub/assets/details/1`. If the user's existing project has the old extension point ID, update both this file and the directory name. + +--- + +## `src/aem-assets-contenthub-1/ext.config.yaml` + +```yaml +$schema: https://unpkg.com/@adobe/aio-schemas@latest/schemas/aio.schema.json +actions: actions +web: web-src +runtimeManifest: + packages: + aem-assets-contenthub-1: + license: Apache-2.0 + actions: + generic: + function: actions/generic/index.js + web: 'yes' + runtime: 'nodejs:18' + inputs: + LOG_LEVEL: debug + annotations: + require-adobe-auth: false + final: true +operations: + view: + - type: web + impl: index.html +hooks: + post-app-deploy: ./hooks/post-deploy.js +``` + +--- + +## `extension-manifest.json` + +```json +{ + "name": "{{EXTENSION_NAME}}", + "id": "aem-assets-contenthub-1", + "description": "{{EXTENSION_DESCRIPTION}}", + "version": "1.0.0", + "engines": { + "aio-cli": ">=10.0.0" + }, + "keywords": ["contenthub", "assets", "extension", "uix"], + "author": "", + "license": "Apache-2.0", + "extensionPoints": ["aem/assets/contenthub/1"] +} +``` + +--- + +## `.eslintrc.js` + +```js +module.exports = { + root: true, + extends: ['@adobe/eslint-config-aio-lib-config'], + env: { + node: true, + es2020: true, + browser: true + }, + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + ecmaFeatures: { jsx: true } + } +}; +``` + +--- + +## `AGENTS.md` + +```markdown +# {{DISPLAY_NAME}} — Agent Guidelines + +## Extension Point + +`aem/assets/contenthub/1` — Content Hub extensibility. + +## Active Namespaces + +- `assetDetails` — tab panels in the Asset Details Dialog side rail +- `card` — action buttons on asset cards (Assets grid, collection, link share) and on collection tiles in the Collections grid +- `selectionBar` — bulk action buttons in the selection bar (multi-select) +- `addAssets` — wizard panels before/after the Upload step; `beforeUpload` hook to gate/enrich uploads; `onUploadComplete` hook after upload finishes + +(Remove entries for namespaces not wired in ExtensionRegistration.js) + +## Project Structure + +- `src/aem-assets-contenthub-1/web-src/` — React SPA (Spectrum UI, UIX Guest) +- `src/aem-assets-contenthub-1/actions/` — Adobe I/O Runtime web actions +- `app.config.yaml` — declares `aem/assets/contenthub/1` extension point +- `src/aem-assets-contenthub-1/ext.config.yaml` — runtime manifest + +## Building & Running + +Use `aio` CLI commands (not npm scripts directly): +- `aio app build` — build and deploy actions to I/O Runtime, compile web-src +- `aio app run` — start local dev server at https://localhost:9080 +- `aio app deploy` — full deployment to Stage/Production + +## Local Test URL + +``` +https://experience.adobe.com/?devMode=true&ext=https://localhost:9080#/assets/contenthub/ +``` + +First time: accept the self-signed cert at https://localhost:9080. + +## Key Files to Customize + +- `ExtensionRegistration.js` — declares all namespaces. Uses `let guestConnection` so `onActionClick` can call `host.modal.openDialog()` after `register()` resolves. +- `TabPanel.js` — panel UI for `assetDetails` namespace (rendered in iframe) +- `CardActionModal.js` — modal UI for `card` namespace; reads its data from the `contentUrl` query (`URLSearchParams`), NOT `getPayload()` +- `SelectionBarModal.js` — modal UI for `selectionBar` namespace; reads `assetIds[]` from the `contentUrl` query (`URLSearchParams` + `JSON.parse`), NOT `getPayload()` +- `actions/generic/index.js` — server-side logic for AEM API calls + +## Host APIs (via `guestConnection.host` in any attached page) + +- `auth.getIMSInfo()` → `{ imsOrg, imsOrgName, accessToken }` +- `auth.getApiKey()` → API key string ← note: `getApiKey`, not `getAPIKey` +- `discovery.getAemHost()` → `https:///` +- `toast.display({ variant, message })` → show toast; variants: `neutral`, `positive`, `negative`, `info` +- `i18n.getLocalizationInfo()` → `{ locale }` +- `modal.openDialog({ title, contentUrl, type?, size?, payload? })` → open modal dialog. **Single config object from the guest's side** — the UIX host auto-injects `{ id }`; never pass it yourself. Pass data to the modal either via the `contentUrl` query string (e.g. `/#card-action-modal?resourceId=...&resourceType=...`) or via `payload`. +- `modal.getPayload()` → returns the `payload` passed to `openDialog()`. Call from the modal page after `attach()`. +- `modal.closeDialog()` → close the current modal (call from the modal page after `attach()`) + +## assetDetails: getCurrentAsset() + +`assetDetails.getCurrentAsset()` returns the asset id as a plain **STRING** (e.g. `"urn:aaid:aem:..."`), NOT an object. Use it directly or wrap: `const asset = { id: assetId }`. + +## card: getActionButtons receives actionContext + +The host calls `getActionButtons(actionContext)` with: +- `actionContext.context`: `'assets'` | `'collection'` | `'collections'` | `'share'` — the source view. + `assets`, `collection`, and `share` are asset-card surfaces; `collections` is a collection tile + on the Collections grid. Use it to vary buttons per surface, or ignore it to show the same set. + +The same `card` namespace serves both asset cards and collection tiles — distinguish them via +`actionContext.context` (and `resourceType` on click). + +## card: onActionClick signature + +Called by the host as `onActionClick(resourceType, buttonId, resourceId, actionContext)`: +- `resourceType`: `'asset'` (asset cards) or `'collection'` (collection tiles) +- `buttonId`: the `id` from `getActionButtons()` +- `resourceId`: the asset or collection URN string +- `actionContext`: `{ context }` — same surface values as above + +## selectionBar: getActionButtons receives actionContext + +The host calls `selectionBar.getActionButtons(actionContext)` with: +- `actionContext.context`: `'assets'` | `'collections'` | `'collection'` | `'share'` — the source view +- `actionContext.resourceSelection.resources`: `[{id: string}, ...]` — the current selection + +Use this to conditionally show/hide buttons depending on where the bar appears, or ignore it. + +## selectionBar: onActionClick signature + +Called by the host as `onActionClick(buttonId, assetIds)`: +- `buttonId`: the `id` from `getActionButtons()` +- `assetIds`: `string[]` — array of selected asset URNs + +Note: the host prefixes the rendered button id as `ext::` to avoid collisions with native actions. Your extension code uses the original `btn.id` — the prefix is host-internal only. +``` + +--- + +## `hooks/post-deploy.js` + +```js +const chalk = require('chalk'); +const fs = require('fs'); +const yaml = require('js-yaml'); + +module.exports = (config) => { + try { + const yamlFile = fs.readFileSync(`${config.root}/app.config.yaml`, 'utf8'); + const yamlData = yaml.load(yamlFile); + const { extensions } = yamlData; + const extension = Object.keys(extensions)[0]; + const previewData = { + extensionPoint: extension, + url: config.project.workspace.app_url, + }; + const base64EncodedData = Buffer.from(JSON.stringify(previewData)).toString('base64'); + console.log(chalk.magenta(chalk.bold('For a developer preview of your UI extension in the Content Hub environment, follow the URL:'))); + const env = process.env.AIO_CLI_ENV === 'stage' ? '-stage' : ''; + console.log(chalk.magenta(chalk.bold(` -> https://experience${env}.adobe.com/aem/extension-manager/preview/${base64EncodedData}`))); + } catch (_) { + // Non-fatal: just skip the preview URL + } +}; +``` + +--- + +## `src/aem-assets-contenthub-1/web-src/index.html` + +```html + + + + + + {{EXTENSION_NAME}} + + + +
+ + + +``` + +--- + +## `src/aem-assets-contenthub-1/web-src/src/index.js` + +```js +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './components/App.js'; +import './index.css'; + +const root = createRoot(document.getElementById('root')); +root.render(); +``` + +--- + +## `src/aem-assets-contenthub-1/web-src/src/index.css` + +```css +html, body { + margin: 0; +} +``` + +--- + +## `src/aem-assets-contenthub-1/web-src/src/config.json` + +```json +{ + "aem-assets-contenthub-1/generic": "https://localhost:9080/api/v1/web/aem-assets-contenthub-1/generic" +} +``` + +This file is overwritten by `aio app run` (localhost URL) and `aio app deploy` (cloud URL). Do not edit manually. + +--- + +## `src/aem-assets-contenthub-1/web-src/src/components/Constants.js` + +```js +export const extensionId = 'sample-extension'; +``` + +The `extensionId` **must** be identical in `register()` (ExtensionRegistration.js) and `attach()` (TabPanel.js). Both import from this file. + +--- + +## `src/aem-assets-contenthub-1/web-src/src/components/App.js` + +Include only the imports and routes for the namespaces selected in Step 2. Remove unused imports/routes. + +```js +import React from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; +import { HashRouter as Router, Routes, Route } from 'react-router-dom'; +import ExtensionRegistration from './ExtensionRegistration'; +import TabPanel from './TabPanel'; // assetDetails namespace — remove if not selected +import CardActionModal from './CardActionModal'; // card namespace — remove if not selected +import SelectionBarModal from './SelectionBarModal'; // selectionBar namespace — remove if not selected +import AddAssetsPanel from './AddAssetsPanel'; // addAssets namespace — remove if not selected + +function App() { + return ( + + + + } /> + } /> + {/* assetDetails namespace — remove if not selected */} + } /> + {/* card namespace — remove if not selected */} + } /> + {/* selectionBar namespace — remove if not selected */} + } /> + {/* addAssets namespace — remove both routes if not selected */} + } /> + } /> + + + + ); + + function onError(e, componentStack) { + console.error('Extension error:', e, componentStack); + } + + function fallbackComponent({ componentStack, error }) { + return ( + +

Extension rendering error

+
{componentStack + '\n' + error.message}
+
+ ); + } +} + +export default App; +``` + +**Adding more panels:** Add a new `} />` here, and a matching `contentUrl: '/#my-second-panel'` entry in `ExtensionRegistration.js`. + +--- + +## `src/aem-assets-contenthub-1/web-src/src/components/ExtensionRegistration.js` + +Contains all four Content Hub namespaces. **When scaffolding, include only the namespace blocks the user selected in Step 2.** Each block is clearly marked — remove unused ones. `card` and `selectionBar` require `let guestConnection` (not `const`) because `onActionClick` is called after `register()` resolves. `addAssets` does NOT need `guestConnection` — its lifecycle hooks run synchronously without calling host APIs. + +```js +import React from 'react'; +import { Text, View } from '@adobe/react-spectrum'; +import { register } from '@adobe/uix-guest'; +import { extensionId } from './Constants'; + +// Restrict extension to specific repos. +// Format: 'delivery-pXXX-eYYY.adobeaemcloud.com' +// Empty array = allow any repo (safe for development; populate before deploying to Production). +const allowedRepos = [ + // 'delivery-p12345-e167890.adobeaemcloud.com', +]; + +function getRepo() { + return new URLSearchParams(window.location.search).get('repo'); +} + +function shouldSkipRegistration(repo) { + return allowedRepos.length > 0 && !allowedRepos.includes(repo); +} + +function ExtensionRegistration() { + const repo = getRepo(); + + if (shouldSkipRegistration(repo)) { + return IFrame for integration with Host (Content Hub), Skipped registration as repo is not allowed; + } + + const init = async () => { + // `let` (not `const`) so card/selectionBar onActionClick handlers can close over it after register() resolves. + let guestConnection = await register({ + id: extensionId, + methods: { + + // ── ASSET DETAILS NAMESPACE ────────────────────────────────────────── + // Tab panels in the Asset Details Dialog side rail. + // Remove this block if assetDetails was not selected. + assetDetails: { + getTabPanels() { + return [ + { + id: '{{EXTENSION_NAME}}-panel', + title: '{{DISPLAY_NAME}}', + tooltip: '{{DISPLAY_NAME}}', + icon: 'Extension', // React-Spectrum workflow icon name + contentUrl: '/#tab-panel', // must match a in App.js + }, + ]; + }, + }, + + // ── ASSET CARD NAMESPACE ───────────────────────────────────────────── + // Buttons on individual asset card menus (3-dot / overlay) AND on collection + // tiles in the Collections grid (the tile's ⋯ menu). Remove this block if card + // was not selected. + // getActionButtons receives an actionContext from the host: + // { context: 'assets'|'collection'|'collections'|'share' } + // 'assets' (browse grid), 'collection' (assets inside a collection), + // 'share' (link share view) are asset-card surfaces; + // 'collections' is a collection tile on the Collections grid. + // onActionClick is called with (resourceType, buttonId, resourceId, actionContext) + // resourceType: 'asset' (asset cards) | 'collection' (collection tiles) + card: { + getActionButtons(actionContext) { + // Vary buttons by actionContext.context, or ignore it to show the same set. + return [ + { + id: '{{EXTENSION_NAME}}-card-action', + label: '{{DISPLAY_NAME}}', + icon: 'Edit', // React-Spectrum workflow icon name + }, + ]; + }, + async onActionClick(resourceType, buttonId, resourceId, actionContext) { + // openDialog takes a SINGLE config object — NO { id } first arg, NO payload field. + // Pass data to the modal via the contentUrl query string (read it there with URLSearchParams). + await guestConnection.host.modal.openDialog({ + title: '{{DISPLAY_NAME}}', + contentUrl: `/#card-action-modal?resourceId=${encodeURIComponent(resourceId)}&resourceType=${encodeURIComponent(resourceType)}`, // route in App.js + query data + type: 'modal', + size: 'M', + }); + }, + }, + + // ── SELECTION BAR NAMESPACE ────────────────────────────────────────── + // Bulk action buttons in the selection bar (shown when assets are selected). + // Remove this block if selectionBar was not selected. + // + // getActionButtons receives an actionContext from the host: + // { context: 'assets'|'collections'|'collection'|'share', + // resourceSelection: { resources: [{id: string}, ...] } } + // Use it to conditionally show/hide buttons per source, or ignore it to always show. + // + // onActionClick is called by the host with (buttonId, assetIds[]) + selectionBar: { + getActionButtons(actionContext) { + // actionContext.context tells you where the selection bar is shown: + // 'assets' (browse grid), 'collections' (collections list), + // 'collection' (inside a collection), 'share' (link share view). + // actionContext.resourceSelection.resources is the current selection as [{id}, ...]. + return [ + { + id: '{{EXTENSION_NAME}}-bulk-action', + label: '{{DISPLAY_NAME}}', + icon: 'Download', // React-Spectrum workflow icon name + }, + ]; + }, + async onActionClick(buttonId, assetIds) { + // Single config object — NO { id }, NO payload. Pass assetIds via the contentUrl query. + const ids = encodeURIComponent(JSON.stringify(assetIds || [])); + await guestConnection.host.modal.openDialog({ + title: `{{DISPLAY_NAME}} (${assetIds.length} asset${assetIds.length !== 1 ? 's' : ''})`, + contentUrl: `/#selection-bar-modal?assetIds=${ids}`, // route in App.js + query data + type: 'modal', + size: 'M', + }); + }, + }, + + // ── ADD ASSETS NAMESPACE ───────────────────────────────────────────── + // Extends the "Add Assets" (hydration) flow with wizard panels and lifecycle hooks. + // Remove this block if addAssets was not selected. + // + // getPanels() returns panels injected before ('pre') or after ('post') the Upload step. + // 'pre' panels gate the Next button — the panel must send a postMessage to unlock it. + // 'post' panels are always ready (no signal required). + // + // beforeUpload(ctx) is called before upload starts: + // ctx: { files, metadata, uploadPath } + // return { proceed: true, metadata } to pass through (optionally merging extra metadata) + // return { proceed: false, message: '...' } to block the upload with a user-facing message + // + // onUploadComplete(ctx) fires after upload finishes (parallel, fire-and-forget). + addAssets: { + getPanels() { + return [ + { + id: '{{EXTENSION_NAME}}-pre-step', + title: '{{DISPLAY_NAME}} Pre-Upload', + position: 'pre', // shown BEFORE the Upload step + contentUrl: '/#pre-step', // must match a in App.js + }, + // Add more panels here, or remove this array entry if you only need post panels. + { + id: '{{EXTENSION_NAME}}-post-step', + title: '{{DISPLAY_NAME}} Post-Upload', + position: 'post', // shown AFTER the Upload step + contentUrl: '/#post-step', + }, + ]; + }, + async beforeUpload({ files, metadata, uploadPath }) { + // Return { proceed: true, metadata } to continue (optionally inject extra metadata). + // Return { proceed: false, message: '...' } to block with a user-facing error. + return { proceed: true, metadata }; + }, + async onUploadComplete({ files, metadata, uploadPath }) { + // Fires after all files finish uploading (fire-and-forget, return value ignored). + console.log('{{EXTENSION_NAME}} onUploadComplete', { fileCount: files.length, uploadPath }); + }, + }, + + }, + }); + }; + + init().catch(console.error); + return ( + + + + Certificate accepted! + + + Return to Claude Code in your terminal and click{' '} + "Done — open the extension" to continue. + + + ); +} + +export default ExtensionRegistration; +``` + +--- + +## `src/aem-assets-contenthub-1/web-src/src/components/TabPanel.js` + +This is the panel content rendered inside Content Hub's iframe. All Host API calls go through `guestConnection.host`. + +```js +import React, { useState, useEffect } from 'react'; +import { attach } from '@adobe/uix-guest'; +import { + Provider, + defaultTheme, + View, + Heading, + Text, + Button, + ProgressCircle, + Divider, +} from '@adobe/react-spectrum'; +import { extensionId } from './Constants'; +import actions from '../config.json'; + +export default function TabPanel() { + const [guestConnection, setGuestConnection] = useState(null); + const [asset, setAsset] = useState(null); + const [loading, setLoading] = useState(true); + const [actionResponse, setActionResponse] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + (async () => { + try { + // Reconnect to the registered extension. + // extensionId must match the id used in register() — both come from Constants.js. + const connection = await attach({ id: extensionId }); + setGuestConnection(connection); + + // getCurrentAsset() returns the asset id as a plain string (e.g. "urn:aaid:aem:..."). + // Note: this is Content Hub's API. Assets View uses a different method. + const assetId = await connection.host.assetDetails.getCurrentAsset(); + setAsset({ id: assetId }); + + // ── Call a web action with the asset ID ──────────────────────────── + // Uncomment and customize for AEM API calls: + // + // const { accessToken, imsOrg } = await connection.host.auth.getIMSInfo(); + // const apiKey = await connection.host.auth.getApiKey(); + // const aemHost = await connection.host.discovery.getAemHost(); + // const actionUrl = actions['aem-assets-contenthub-1/generic']; + // + // const response = await fetch(actionUrl, { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json', + // // Add these when action has require-adobe-auth: true: + // // 'Authorization': `Bearer ${accessToken}`, + // // 'x-gw-ims-org-id': imsOrg, + // }, + // body: JSON.stringify({ assetId: currentAsset.id, aemHost, apiKey, imsOrg }), + // }); + // const data = await response.json(); + // setActionResponse(data); + + } catch (err) { + console.error('Panel initialization error:', err); + setError('Failed to initialize panel: ' + err.message); + } finally { + setLoading(false); + } + })(); + }, []); + + function displayToast(variant, message) { + if (guestConnection) { + guestConnection.host.toast.display({ variant, message }); + } + } + + if (loading) { + return ( + + + + + + + + ); + } + + return ( + + + {{DISPLAY_NAME}} + + + + {error && ( + + {error} + + )} + + {asset && ( + + Asset ID: + + + {asset.id} + + + + )} + + {actionResponse && ( + + + Action Response: + + {JSON.stringify(actionResponse, null, 2)} + + + )} + + + + + ); +} +``` + +--- + +## `src/aem-assets-contenthub-1/web-src/src/components/CardActionModal.js` + +Modal opened when a card action button is clicked. Reads the data passed via the `contentUrl` query string (the simpler alternative to `openDialog()`'s `payload`/`getPayload()` — either works), and uses `attach()` only so it can call `closeDialog()`. Only scaffold this file if `card` was selected in Step 2. + +```js +import React, { useState, useEffect } from 'react'; +import { attach } from '@adobe/uix-guest'; +import { + Provider, + defaultTheme, + View, + Text, + Button, + ProgressCircle, +} from '@adobe/react-spectrum'; +import { extensionId } from './Constants'; + +export default function CardActionModal() { + const [guestConnection, setGuestConnection] = useState(null); + const [payload, setPayload] = useState(null); + + useEffect(() => { + (async () => { + // Read data from the modal URL query (alternative: openDialog()'s payload + host.modal.getPayload()). + // contentUrl was `/#card-action-modal?resourceId=...&resourceType=...`. + const params = new URLSearchParams(window.location.hash.split('?')[1] || ''); + setPayload({ resourceId: params.get('resourceId'), resourceType: params.get('resourceType') }); + // attach() is only needed so the Close button can call host.modal.closeDialog(). + const connection = await attach({ id: extensionId }); + setGuestConnection(connection); + })(); + }, []); + + if (!payload) { + return ( + + + + + + + + ); + } + + return ( + + + + + Resource Type + + {payload.resourceType} + + + + + Resource ID + + + + {payload.resourceId} + + + + + {/* Add your custom UI here */} + + + + + ); +} +``` + +--- + +## `src/aem-assets-contenthub-1/web-src/src/components/SelectionBarModal.js` + +Modal opened when a selection bar (bulk action) button is clicked. Reads `assetIds[]` from the `contentUrl` query (the simpler alternative to `openDialog()`'s `payload`/`getPayload()` — either works). Only scaffold this file if `selectionBar` was selected in Step 2. + +```js +import React, { useState, useEffect } from 'react'; +import { attach } from '@adobe/uix-guest'; +import { + Provider, + defaultTheme, + View, + Text, + Button, + ProgressCircle, + ListView, + Item, +} from '@adobe/react-spectrum'; +import { extensionId } from './Constants'; + +export default function SelectionBarModal() { + const [guestConnection, setGuestConnection] = useState(null); + const [payload, setPayload] = useState(null); + + useEffect(() => { + (async () => { + // Read assetIds from the modal URL query (alternative: openDialog()'s payload + host.modal.getPayload()). + // contentUrl was `/#selection-bar-modal?assetIds=`. + const params = new URLSearchParams(window.location.hash.split('?')[1] || ''); + const raw = params.get('assetIds'); + setPayload({ assetIds: raw ? JSON.parse(raw) : [] }); + // attach() is only needed so the Close button can call host.modal.closeDialog(). + const connection = await attach({ id: extensionId }); + setGuestConnection(connection); + })(); + }, []); + + if (!payload) { + return ( + + + + + + + + ); + } + + return ( + + + + + {payload.assetIds.length} asset{payload.assetIds.length !== 1 ? 's' : ''} selected + + + + + ({ id }))}> + {item => ( + + {item.id} + + )} + + + + {/* Add your bulk-action logic here */} + + + + + ); +} +``` + +--- + +## `src/aem-assets-contenthub-1/web-src/src/components/AddAssetsPanel.js` + +Wizard panel rendered inside a `GuestUIFrame` for the `addAssets` namespace. Used for both `pre` and `post` position panels (same component, different route). + +**Critical:** `getPanelId()` strips the leading `#` (and optional `/`) from `window.location.hash`. +`contentUrl: '/#pre-step'` loads as hash `#pre-step` (NOT `#/pre-step`) — using `.replace('#/', '')` silently fails and sends the wrong `panelId` in the `postMessage`, keeping Next permanently disabled. +Always use the regex `/^#\/?/` as shown below. + +```js +import React, { useState, useEffect } from 'react'; +import { attach } from '@adobe/uix-guest'; +import { + Provider, + defaultTheme, + View, + Heading, + Text, + Button, + ProgressCircle, +} from '@adobe/react-spectrum'; +import { extensionId } from './Constants'; + +// contentUrl '/#pre-step' loads with hash '#pre-step' (no slash after #). +// Use /^#\/?/ to strip '#' or '#/' — plain '#/' replace silently fails and breaks the panelId. +function getPanelId() { + const route = window.location.hash.replace(/^#\/?/, '').split('?')[0]; + return `{{EXTENSION_NAME}}-${route}`; +} + +function isPrePanel() { + return window.location.hash.includes('pre-step'); +} + +export default function AddAssetsPanel() { + const [guestConnection, setGuestConnection] = useState(null); + const [ready, setReady] = useState(false); + const panelId = getPanelId(); + const isPre = isPrePanel(); + + useEffect(() => { + (async () => { + const connection = await attach({ id: extensionId }); + setGuestConnection(connection); + // Post panels default to ready (no signalling required). + if (!isPre) setReady(true); + })(); + }, []); + + function markReady() { + // Signal the wizard host: Next button becomes clickable for this panel. + // panelId must exactly match the id returned by getPanels() in ExtensionRegistration.js. + window.parent.postMessage( + { type: 'addAssets:setReadyToAdvance', panelId, ready: true }, + '*' + ); + setReady(true); + } + + if (!guestConnection) { + return ( + + + + + + + + ); + } + + return ( + + + + {{DISPLAY_NAME}} — {isPre ? 'Pre-Upload' : 'Post-Upload'} + + + + + {isPre + ? 'Complete any required steps before uploading your assets, then click "Ready to Upload" to proceed.' + : 'Your assets have been uploaded. Review or tag them below.'} + + + + {/* Add your custom wizard panel UI here */} + + {isPre && !ready && ( + + )} + + {ready && ( + + ✓ {isPre ? 'Ready to proceed to upload.' : 'Upload complete.'} + + )} + + + ); +} +``` + +--- + +## `src/aem-assets-contenthub-1/actions/utils.js` + +```js +function errorResponse(statusCode, message) { + return { statusCode, body: { error: message } }; +} + +function getBearerToken(params) { + const authHeader = params.__ow_headers?.authorization || params.authorization; + if (authHeader && authHeader.startsWith('Bearer ')) { + return authHeader.substring(7); + } + throw new Error('Missing or invalid authorization header'); +} + +function checkMissingRequestInputs(params, requiredParams) { + const missing = requiredParams.filter(param => !params[param]); + if (missing.length > 0) { + return `Missing required parameters: ${missing.join(', ')}`; + } + return null; +} + +module.exports = { errorResponse, getBearerToken, checkMissingRequestInputs }; +``` + +--- + +## `src/aem-assets-contenthub-1/actions/generic/index.js` + +The web action called by the panel. Customize with real AEM API logic. + +```js +const { errorResponse, getBearerToken, checkMissingRequestInputs } = require('../utils'); + +/** + * Generic Web Action — {{DISPLAY_NAME}} + * + * Called from TabPanel.js with the current asset ID. + * Customize this to call AEM Assets Author API. + * + * Params passed from the panel: + * assetId — asset urn from host.assetDetails.getCurrentAsset() + * aemHost — AEM author URL from host.discovery.getAemHost() + * apiKey — from host.auth.getApiKey() (never hardcode) + * imsOrg — from host.auth.getIMSInfo() + * + * To call authenticated AEM APIs: + * 1. Set require-adobe-auth: true in ext.config.yaml + * 2. Send Authorization header from the panel + * 3. Uncomment the fetch block below + */ +async function main(params) { + console.log('{{EXTENSION_NAME}} action called', JSON.stringify({ assetId: params.assetId }, null, 2)); + + try { + // Uncomment for authenticated AEM API calls: + // const token = getBearerToken(params); + // const { assetId, aemHost, apiKey, imsOrg } = params; + // + // const response = await fetch(`https://${aemHost}/adobe/assets/${assetId}/metadata`, { + // headers: { + // 'Authorization': `Bearer ${token}`, + // 'X-Api-Key': apiKey, // always from frontend — never hardcode + // 'x-gw-ims-org-id': imsOrg, + // 'Content-Type': 'application/json', + // }, + // }); + // const data = await response.json(); + // const metadata = data.value ?? data; + // return { statusCode: 200, body: { metadata } }; + + return { + statusCode: 200, + body: { + message: 'Action executed successfully', + extension: '{{EXTENSION_NAME}}', + timestamp: new Date().toISOString(), + assetId: params.assetId || null, + }, + }; + + } catch (error) { + console.error('Action error:', error); + return errorResponse(500, `Action failed: ${error.message}`); + } +} + +exports.main = main; +``` + +--- + +## `README.md` + +```markdown +# {{DISPLAY_NAME}} + +Content Hub UI extension for the `aem/assets/contenthub/1` extension point. + +## Extension Point + +`aem/assets/contenthub/1` — adds custom tab panels to the Asset Details Dialog. + +## Project Structure + +``` +src/aem-assets-contenthub-1/ + web-src/src/components/ + ExtensionRegistration.js — registers tab panels with Content Hub + TabPanel.js — custom panel UI (rendered in iframe) + App.js — React routing + Constants.js — extensionId (shared between register + attach) + actions/ + generic/index.js — I/O Runtime web action (AEM API calls) + utils.js — shared action utilities + ext.config.yaml — runtime manifest + action definitions +app.config.yaml — declares aem/assets/contenthub/1 extension point +``` + +## Development + +```bash +npm install +aio app build +aio app run +``` + +Test URL: +``` +https://experience.adobe.com/?devMode=true&ext=https://localhost:9080#/assets/contenthub/ +``` + +No `&repo=` needed for local dev — the scaffold sets `allowedRepos = []`, so any repo (or none) works. + +> First time only: accept the self-signed cert at https://localhost:9080 before loading the test URL. + +## Allowed Repos + +Before deploying to Production, update `allowedRepos` in `ExtensionRegistration.js` with your delivery repo IDs — this restricts which AEM repos may load the extension: + +```js +const allowedRepos = ['delivery-p12345-e167890.adobeaemcloud.com']; +``` + +## Deploy + +**Stage:** `aio app use -w Stage && aio app deploy` + +**Production:** `aio app use -w Production && aio app deploy` + +Approve in [Extension Manager](https://experience.adobe.com/aem/extension-manager) after deploying to Production. +``` + +--- + +## Adding a Second Panel + +When the user wants a second tab panel, make these changes: + +### In `ExtensionRegistration.js` — add to the `getTabPanels()` array: + +```js +getTabPanels() { + return [ + { + id: '{{EXTENSION_NAME}}-panel', + title: '{{DISPLAY_NAME}}', + tooltip: '{{DISPLAY_NAME}}', + icon: 'Extension', + contentUrl: '/#tab-panel', + }, + { + id: '{{EXTENSION_NAME}}-second-panel', + title: 'Second Panel', + tooltip: 'Second Panel', + icon: 'Info', + contentUrl: '/#second-panel', // must match the route added in App.js + }, + ]; +}, +``` + +### In `App.js` — add a route: + +```js +import SecondPanel from './SecondPanel'; // create this component + +} /> +``` + +### Create `SecondPanel.js` — copy the `TabPanel.js` template, rename the component and customize the UI. diff --git a/plugins/app-builder/skills/appbuilder-testing/SKILL.md b/plugins/app-builder/skills/appbuilder-testing/SKILL.md index 6e5db2f2..354a411f 100644 --- a/plugins/app-builder/skills/appbuilder-testing/SKILL.md +++ b/plugins/app-builder/skills/appbuilder-testing/SKILL.md @@ -4,13 +4,14 @@ description: >- Generate and run tests for Adobe App Builder actions and UI components. Scaffolds Jest unit tests, integration tests against deployed actions, contract tests for Adobe API interactions, and React component tests using Testing Library. Provides mock helpers for State, Files, Events SDKs, - @adobe/aio-lib-* clients, ExC Shell context (@adobe/exc-app), and UIX Guest SDK (@adobe/uix-guest). - Use this skill whenever the user mentions testing App Builder actions, writing unit tests for - Runtime actions, creating integration tests, mocking Adobe SDKs, setting up test fixtures, running - aio app test, or wants to verify action behavior before deployment. Also trigger when users mention - Jest configuration for App Builder, test coverage, CI test setup, React component test, Testing - Library, UI test, Provider wrapper, test my page, test my form, test my table, test my component, - mock shell context, mock extension context, debug test failures, or fix Jest errors. + @adobe/aio-lib-* clients, ExC Shell context (@adobe/exc-app), and UIX Guest SDK (@adobe/uix-guest) + for AEM and Content Hub extension components (ExtensionRegistration, TabPanel, CardActionModal, + SelectionBarModal). Use this skill whenever the user mentions testing App Builder actions, writing + unit tests for Runtime actions, creating integration tests, mocking Adobe SDKs, setting up test + fixtures, running aio app test, or wants to verify action behavior before deployment. Also trigger + when users mention Jest configuration for App Builder, test coverage, CI test setup, React component + test, Testing Library, UI test, test my Content Hub extension, mock shell/extension context, debug + test failures, or fix Jest errors. metadata: category: testing license: Apache-2.0 @@ -40,6 +41,7 @@ Pick the template or reference that matches the user's intent. Default to `asset | Contract test for API interactions | references/testing-patterns.md | — | | Pre-deployment verification | references/checklist.md | — | | Debug test failures | references/debugging.md | — | +| Content Hub extension test (ExtensionRegistration, TabPanel, card/selection-bar modals) | references/testing-patterns.md | assets/uix-guest-mock-helper.js | ## Fast Path (for clear requests) diff --git a/plugins/app-builder/skills/appbuilder-ui-scaffolder/SKILL.md b/plugins/app-builder/skills/appbuilder-ui-scaffolder/SKILL.md index 6feaa04e..6e75d0f7 100644 --- a/plugins/app-builder/skills/appbuilder-ui-scaffolder/SKILL.md +++ b/plugins/app-builder/skills/appbuilder-ui-scaffolder/SKILL.md @@ -5,11 +5,13 @@ description: >- Provides patterns for pages, forms, data tables, dialogs, and navigation using @adobe/react-spectrum. Guides ExC Shell integration with @adobe/exc-app including runtime.done(), IMS token passthrough, and shell theming. Guides AEM UI Extension development with @adobe/uix-guest for Content Fragment - Console, CF Editor, Universal Editor, and Assets View surfaces. Trigger on: building App Builder UI, - React Spectrum components, ExC Shell pages, forms, data tables, dialogs, modals, navigation, theming, - web-src, Spectrum design system, @adobe/exc-app, AEM extension, AEM UI extension, Content Fragment - Console, Universal Editor extension, uix-guest, @adobe/uix-guest, extension points for AEM, - customizing AEM surfaces. + Console, CF Editor, Universal Editor, Assets View, and Content Hub surfaces (Content Hub via + aem/assets/contenthub/1 — asset details panels, card actions, bulk actions, Add Assets wizard). + Trigger on: building App Builder UI, React Spectrum components, ExC Shell pages, forms, data tables, + dialogs, modals, navigation, theming, web-src, Spectrum, @adobe/exc-app, AEM extension, + AEM UI extension, Content Fragment Console, Universal Editor extension, Content Hub panel, Content Hub + card action, Content Hub bulk action, add assets wizard, uix-guest, @adobe/uix-guest, extension points + for AEM, customizing AEM surfaces. metadata: category: frontend license: Apache-2.0 @@ -33,7 +35,8 @@ Identify the user's intent, then read the referenced sections to generate tailor | Navigation layout | `references/ui-patterns.md` § Navigation | `Tabs`, `Breadcrumbs`, `Flex` | | ExC Shell setup | `references/shell-integration.md` | `@adobe/exc-app`, `Provider`, `defaultTheme` | | Connect UI to backend actions | `references/action-integration.md` | `fetch()` with IMS token | -| AEM UI Extension (CF Console, CF Editor, Universal Editor) | `references/aem-extensions.md` | `@adobe/uix-guest`, `register()`, `sharedContext` | +| AEM UI Extension (CF Console, CF Editor, Universal Editor, Assets View) | `references/aem-extensions.md` | `@adobe/uix-guest`, `register()`, `sharedContext` | +| Content Hub extension (panels, card actions, bulk actions, add assets wizard) | `references/aem-extensions.md` § Content Hub | `@adobe/uix-guest`, `register()`, `attach()`, `assetDetails`/`card`/`selectionBar`/`addAssets` namespaces, `host.modal.openDialog`, `postMessage` readiness signalling | | Debug UI issues | `references/debugging.md` | Shell spinner, CORS, blank screen, auth | ## Fast Path (for clear requests) @@ -55,6 +58,8 @@ Examples of fast-path triggers: - "Build a Content Fragment Console extension" → Read `references/aem-extensions.md` § CF Console, generate directly - "Add a header menu button to the Universal Editor" → Read `references/aem-extensions.md` § Universal Editor, generate directly - "Create an AEM extension with uix-guest" → Read `references/aem-extensions.md` § Core Registration, generate directly +- "Add a Content Hub panel / card action / bulk action / add-assets step" → Read `references/aem-extensions.md` § Content Hub, generate directly +- "Create / scaffold a new extension from scratch" → Redirect to the `appbuilder-project-init` skill (Console setup, template selection, file generation, dev server) If there is any ambiguity — multiple patterns could fit, constraints are unclear, or the user hasn't specified enough — fall through to the full workflow below. @@ -93,6 +98,11 @@ If there is any ambiguity — multiple patterns could fit, constraints are uncle - "Build a Content Fragment Console extension with an action bar button." - "Add a custom RTE toolbar button in the Content Fragment Editor." - "Create a Universal Editor extension with a header menu button." +- "Add a custom panel to the Content Hub Asset Details Dialog." +- "Build a Content Hub extension that shows asset metadata in a side panel." +- "Add an action button to Content Hub asset cards." +- "Add a bulk action to the Content Hub selection bar." +- "Customize the Content Hub extension UI with React Spectrum." ## Inputs To Request @@ -125,12 +135,13 @@ If there is any ambiguity — multiple patterns could fit, constraints are uncle - Use `references/routing-patterns.md` for SPA routing with React Router in ExC Shell. - Use `references/action-integration.md` for calling backend actions from the SPA. - Use `references/checklist.md` for pre-handoff UI quality validation. -- Use `references/aem-extensions.md` for AEM UI Extension patterns (`@adobe/uix-guest`, Content Fragment Console/Editor, Universal Editor, Assets View). +- Use `references/aem-extensions.md` for AEM UI Extension patterns (`@adobe/uix-guest`) — Content Fragment Console/Editor, Universal Editor, Assets View, and Content Hub (`aem/assets/contenthub/1`: asset details tab panels, card/selection-bar/add-assets namespaces, Host APIs, web actions). - Use `references/debugging.md` for common SPA debugging scenarios (shell spinner, CORS, auth, blank screen, performance). ## Chaining -- Chains FROM `appbuilder-project-init` (after SPA project is scaffolded with `dx/excshell/1` extension) -- Works alongside `appbuilder-action-scaffolder` for full-stack features (UI calls backend actions) -- Chains TO `appbuilder-testing` (test generated UI components) -- Chains TO `appbuilder-cicd-pipeline` (deploy frontend changes) +- Chains FROM `appbuilder-project-init` — once the extension is scaffolded and running locally, this skill handles UI customization and React Spectrum patterns +- Works alongside `appbuilder-action-scaffolder` for full-stack features (UI calls backend actions — including Content Hub web actions) +- Chains TO `appbuilder-testing` (unit/component tests for the generated UI) +- Chains TO `appbuilder-cicd-pipeline` (GitHub Actions deployment of frontend changes) +- Chains TO `appbuilder-e2e-testing` (Playwright E2E tests against the deployed extension) diff --git a/plugins/app-builder/skills/appbuilder-ui-scaffolder/references/aem-extensions.md b/plugins/app-builder/skills/appbuilder-ui-scaffolder/references/aem-extensions.md index 33cd0f05..f506ae67 100644 --- a/plugins/app-builder/skills/appbuilder-ui-scaffolder/references/aem-extensions.md +++ b/plugins/app-builder/skills/appbuilder-ui-scaffolder/references/aem-extensions.md @@ -1,6 +1,6 @@ # AEM UI Extension Patterns -Patterns for building AEM UI Extensions using `@adobe/uix-guest`. These extensions customize AEM surfaces (Content Fragment Console, Content Fragment Editor, Universal Editor, Assets View) and run as App Builder apps inside iframes. +Patterns for building AEM UI Extensions using `@adobe/uix-guest`. These extensions customize AEM surfaces (Content Fragment Console, Content Fragment Editor, Universal Editor, Assets View, Content Hub) and run as App Builder apps inside iframes. **Key difference from `@adobe/exc-app`:** ExC Shell apps use `register()` from `@adobe/exc-app` with `runtime.done()`. AEM extensions use `register()` from `@adobe/uix-guest` with a `methods` object that declares extension points. The two are completely separate APIs. @@ -71,6 +71,7 @@ Extension point identifiers: - `aem/cf-editor/1` — Content Fragment Editor - `aem/universal-editor/1` — Universal Editor - `aem/assets/1` — Assets View (requires Assets Ultimate license) +- `aem/assets/contenthub/1` — Content Hub (unified — asset details, card, selection bar, and add-assets surfaces via one `register()` call) --- @@ -412,6 +413,277 @@ Refer to [Assets View extension docs](https://developer.adobe.com/uix/docs/servi --- +## Content Hub Extensions (`aem/assets/contenthub/1`) + +Content Hub is a single extension point that spans **four** surfaces, each opted into via a method namespace in one `register()` call: + +- **Asset Details Dialog** (`assetDetails`) — custom tab panels in the side rail. +- **Asset card actions** (`card`) — buttons on asset cards (Assets grid, inside a collection, link-share) **and** on collection tiles in the Collections grid; the surface is passed in `actionContext.context`. +- **Selection bar / bulk actions** (`selectionBar`) — buttons in the multi-select action bar. +- **Add Assets wizard** (`addAssets`) — inject panels before/after the upload step, gate uploads, react to upload completion. + +> Use the deprecated ID `aem/contenthub/assets/details/1` only for older projects mid-transition; new projects use `aem/assets/contenthub/1`. + +**Auth is different from the other AEM surfaces:** Content Hub does *not* use `sharedContext`. Get auth and environment from the `host` namespaces instead (`host.auth.getIMSInfo()`, `host.discovery.getAemHost()` — see Host APIs below). + +### Registration (all four namespaces) + +```js +import { register } from '@adobe/uix-guest'; + +// `let` (not `const`): card/selectionBar onActionClick handlers reference the +// connection AFTER register() resolves, to open a modal via host.modal.openDialog(). +let guestConnection; + +guestConnection = await register({ + id: 'my.company.extension-name', // reverse-domain; must match attach() calls + methods: { + assetDetails: { + getTabPanels() { /* tab panels in the Asset Details Dialog */ }, + }, + card: { + getActionButtons(actionContext) { /* buttons on asset cards + collection tiles */ }, + async onActionClick(resourceType, buttonId, resourceId, actionContext) { /* … */ }, + }, + selectionBar: { + getActionButtons(actionContext) { /* buttons in the bulk-action bar */ }, + async onActionClick(buttonId, assetIds) { /* … */ }, + }, + addAssets: { + getPanels() { /* wizard panels before/after the Upload step */ }, + async beforeUpload(ctx) { /* gate or enrich metadata before upload */ }, + async onUploadComplete(ctx) { /* react after upload finishes */ }, + }, + }, +}); +``` + +Opt into any combination — only implement the namespaces you use, and scaffold one component per namespace (`TabPanel.js` for `assetDetails`, `CardActionModal.js` for `card`, `SelectionBarModal.js` for `selectionBar`, `AddAssetsPanel.js` for `addAssets`). `card` and `selectionBar` are gated by the `EXTENSIBILITY_AEM_CONTENTHUB` feature flag; when it is off the host renders no buttons for those surfaces, but `assetDetails` panels still render. + +`app.config.yaml` includes the unified extension point once: + +```yaml +extensions: + aem/assets/contenthub/1: + $include: src/aem-assets-contenthub-1/ext.config.yaml +``` + +### Asset Details (`assetDetails`) + +Adds tab panels to the Asset Details Dialog side rail. Content Hub manages toggling, deep-linking, and header rendering — the extension only provides the panel content via a hash route. + +```js +assetDetails: { + getTabPanels() { + return [ + { + id: 'my-panel', // unique within this extension + title: 'My Panel', // panel header (Content Hub renders it) + tooltip: 'My Panel', // side-rail icon tooltip + icon: 'Extension', // React-Spectrum workflow icon name + contentUrl: '/#tab-panel', // hash route — must match a in App.js + }, + ]; + }, +} +``` + +Restrict to specific repos with an allow-list; leave it empty to load for any repo (safe for local dev): + +```js +const allowedRepos = ['delivery-p12345-e167890.adobeaemcloud.com']; +const shouldSkipRegistration = (repo) => allowedRepos.length > 0 && !allowedRepos.includes(repo); +``` + +### Asset Card Actions (`card`) + +Buttons on asset cards (Assets grid / inside a collection / link-share) and on collection tiles. One implementation serves every card surface — the host passes the surface in `actionContext.context`. + +```js +card: { + // actionContext.context: 'assets' | 'collection' | 'share' (asset cards) | 'collections' (collection tiles) + getActionButtons(actionContext) { + return [ + { id: 'my-card-action', label: 'Edit Metadata', icon: 'Edit' }, // card uses `label`, NOT `title` + ]; + }, + // Exact arg order the host uses. Optional (host guards with ?.) — needed to open a modal. + async onActionClick(resourceType, buttonId, resourceId, actionContext) { + // resourceType: 'asset' (cards) | 'collection' (tiles); resourceId: the URN string + await guestConnection.host.modal.openDialog({ + title: 'Edit Metadata', + contentUrl: `/#card-action-modal?resourceId=${encodeURIComponent(resourceId)}&resourceType=${resourceType}`, + type: 'modal', + size: 'M', + }); + }, +} +``` + +Only `id`, `label`, `icon` are read for card buttons. Because `onActionClick` fires *after* `register()` resolves, declare `let guestConnection` so the handler can reference it. + +### Selection Bar / Bulk Actions (`selectionBar`) + +Buttons in the bulk-action bar shown when one or more assets are selected. The signature **differs from `card`**: no `resourceType`, and the click handler receives an **array** of asset IDs. + +```js +selectionBar: { + // actionContext: { context: 'assets'|'collections'|'collection'|'share', + // resourceSelection: { resources: [{ id }, …] } } + getActionButtons(actionContext) { + return [ + { id: 'my-bulk-action', label: 'Bulk Export', icon: 'Download' }, // uses `label`, NOT `title` + ]; + }, + async onActionClick(buttonId, assetIds) { // assetIds: string[] of selected URNs + const ids = encodeURIComponent(JSON.stringify(assetIds || [])); + await guestConnection.host.modal.openDialog({ + title: `Bulk Export (${assetIds.length})`, + contentUrl: `/#selection-bar-modal?assetIds=${ids}`, + type: 'modal', + size: 'M', + }); + }, +} +``` + +The host prefixes selection-bar button ids internally (`ext::`); your code always uses the original `btn.id` — that's what `onActionClick` receives too. + +### Add Assets Wizard (`addAssets`) + +Integrates with the "Add Assets" (hydration) flow: inject wizard steps, gate/enrich uploads, and react to completion. + +```js +addAssets: { + getPanels() { + return [ + { id: 'my-pre-step', title: 'Select Campaign', position: 'pre', contentUrl: '/#pre-step' }, + { id: 'my-post-step', title: 'Tag Assets', position: 'post', contentUrl: '/#post-step' }, + ]; + }, + // Return { proceed:false, message } to block, or { proceed:true, metadata } to merge metadata. + async beforeUpload({ files, metadata, uploadPath }) { + return { proceed: true, metadata: { ...metadata, 'xdm:campaignName': 'Q3 Launch' } }; + }, + // Fires after all files upload (parallel, fire-and-forget). Return value ignored. + async onUploadComplete({ files, metadata, uploadPath }) { /* e.g. post to a webhook */ }, +} +``` + +Step order: `[pre panels] → [Upload step] → [post panels]`. A `pre` panel blocks **Next** by default; its iframe must signal readiness via `postMessage` (there is no `host.modal` in a wizard panel): + +```js +window.parent.postMessage( + { type: 'addAssets:setReadyToAdvance', panelId: 'my-pre-step', ready: true }, + '*' +); +``` + +`post` panels default to ready. For `beforeUpload`, multiple extensions merge (last-writer-wins per key) and any single `{ proceed: false }` blocks the upload. + +### Opening a Modal (`modal`) + +Card and selection-bar actions have no panel of their own — they open a modal whose content is another hash route in the same guest app. **Content Hub's `openDialog` takes a single config object** — you never pass `{ id }` (the UIX host auto-injects the extension id on its side). This is the **opposite** of the other AEM surfaces, which use `host.modal.showUrl({ title, url })` + `close()`. Don't cross them. + +```js +// From an onActionClick handler: +await guestConnection.host.modal.openDialog({ + title: 'Dialog title', + contentUrl: '/#card-action-modal?resourceId=…', // pass data via query string… + type: 'modal', // 'modal' | 'fullscreen' + size: 'M', // 'S' | 'M' | 'L' + // payload: { … }, // …or via payload, read with modal.getPayload() +}); +``` + +Inside the modal page (its own iframe route), read the data and reconnect with `attach()`: + +```js +import { attach } from '@adobe/uix-guest'; +import { extensionId } from './Constants'; + +const params = new URLSearchParams(window.location.hash.split('?')[1] || ''); +const resourceId = params.get('resourceId'); // or: const { resourceId } = await connection.host.modal.getPayload(); +const connection = await attach({ id: extensionId }); +await connection.host.modal.closeDialog(); // dismiss +``` + +### React Routing (`App.js`) + +Hash routing — every panel/modal `contentUrl` must match a ``. Keep only the routes for the namespaces you use. + +```js +import { HashRouter as Router, Routes, Route } from 'react-router-dom'; + + + + } /> + } /> + } /> {/* assetDetails */} + } /> {/* card modal */} + } /> {/* selectionBar modal */} + } /> {/* addAssets */} + } /> + + +``` + +### Host APIs + +All via `guestConnection.host` (from either `register()` or `attach()`); every call returns a Promise. + +```js +const { imsOrg, imsOrgName, accessToken } = await guestConnection.host.auth.getIMSInfo(); +const apiKey = await guestConnection.host.auth.getApiKey(); // never hardcode +const aemHost = await guestConnection.host.discovery.getAemHost(); // "author-p12345-e67890.adobeaemcloud.com" +guestConnection.host.toast.display({ variant: 'positive', message: 'Saved!' }); // neutral|positive|info|negative +const { locale } = await guestConnection.host.i18n.getLocalizationInfo(); +const assetId = await guestConnection.host.assetDetails.getCurrentAsset(); // plain STRING (e.g. "urn:aaid:aem:…") +``` + +### Calling Web Actions from a Panel + +Never call AEM APIs from the browser (CORS blocks them) — route through an App Builder web action: + +```js +// In TabPanel.js +const { accessToken, imsOrg } = await guestConnection.host.auth.getIMSInfo(); +const apiKey = await guestConnection.host.auth.getApiKey(); +const aemHost = await guestConnection.host.discovery.getAemHost(); +const assetId = await guestConnection.host.assetDetails.getCurrentAsset(); + +const response = await fetch(actions['aem-assets-contenthub-1/generic'], { // URL from config.json + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ assetId, aemHost, apiKey, imsOrg }), +}); +``` + +The web action (`actions/generic/index.js`) makes the authenticated AEM Assets Author API call server-side and returns the result. See the `appbuilder-action-scaffolder` skill for the action itself. + +### Local Development + +```bash +aio app build && aio app run +``` + +Test URL: `https://experience.adobe.com/?devMode=true&ext=https://localhost:9080#/assets/contenthub/` + +First run only: navigate to `https://localhost:9080` and accept the self-signed cert, or the panel stays blank. No `&repo=` needed when `allowedRepos = []`. + +### Common Gotchas (Content Hub) + +1. **`openDialog` is a single object** — passing `{ id }` yourself makes the host retry every 500ms until it times out (`… timed out after 10000ms`, `[object Object] doesn't exist`) while `host.toast` still works, masking the cause. Never pass `{ id }`. +2. **`const guestConnection` breaks card/selectionBar** — their `onActionClick` fires after `register()` resolves; use `let` or the handler closes over `undefined`. +3. **`getCurrentAsset()` returns a STRING**, not `{ id }`. (Assets View's `host.details.getCurrentResourceInfo()` is a different shape — don't mix.) +4. **Card/selectionBar buttons use `label`, not `title`** — a button with only `title` renders blank. (`assetDetails` panels use `title`/`tooltip`.) +5. **`card` vs `selectionBar` signatures differ** — card `onActionClick(resourceType, buttonId, resourceId, actionContext)` (single resource); selectionBar `onActionClick(buttonId, assetIds)` (array, no resourceType). +6. **Buttons missing entirely** — `card`/`selectionBar` are gated by the `EXTENSIBILITY_AEM_CONTENTHUB` flag; asset-details panels still show when it's off. +7. **`beforeUpload` must return `{ proceed, metadata }`** — omitting `metadata` loses it; pass `{ proceed: true, metadata: ctx.metadata }` for a no-op, and always include `message` when blocking. +8. **`attach()` id must match `register()` id** — export `extensionId` from `Constants.js` and import it in both. + +--- + ## Extension Testing & Development ### Local Development @@ -482,3 +754,4 @@ extensions: | CF Editor | `aem/cf-editor/1` | `headerMenu`, `rte` | `contentFragment`, `modal`, `toaster` | | Universal Editor | `aem/universal-editor/1` | `headerMenu` | `modal` | | Assets View | `aem/assets/1` | `actionBar`, `headerMenu` | `modal` | +| Content Hub | `aem/assets/contenthub/1` | `assetDetails`, `card`, `selectionBar`, `addAssets` | `auth`, `discovery`, `toast`, `i18n`, `modal` (`openDialog`/`closeDialog`), `assetDetails` | From 98aa7b9d2916c6d759dca3989771f0df05e4b7c6 Mon Sep 17 00:00:00 2001 From: Naradasi Tejaswi Date: Fri, 10 Jul 2026 15:36:13 +0530 Subject: [PATCH 2/6] feat(contenthub): update skills with Content Hub namespace and template changes --- .../appbuilder-action-scaffolder/README.md | 6 +- .../appbuilder-action-scaffolder/SKILL.md | 6 +- .../assets/contenthub-action-template.js | 71 ------- .../skills/appbuilder-project-init/SKILL.md | 10 +- .../references/contenthub-scaffolding.md | 14 +- .../references/contenthub-templates.md | 180 ++---------------- .../skills/appbuilder-testing/SKILL.md | 4 +- .../skills/appbuilder-ui-scaffolder/SKILL.md | 10 +- .../references/aem-extensions.md | 56 +----- 9 files changed, 42 insertions(+), 315 deletions(-) delete mode 100644 plugins/app-builder/skills/appbuilder-action-scaffolder/assets/contenthub-action-template.js diff --git a/plugins/app-builder/skills/appbuilder-action-scaffolder/README.md b/plugins/app-builder/skills/appbuilder-action-scaffolder/README.md index c5d57c9b..ccf7d9ac 100644 --- a/plugins/app-builder/skills/appbuilder-action-scaffolder/README.md +++ b/plugins/app-builder/skills/appbuilder-action-scaffolder/README.md @@ -21,7 +21,7 @@ appbuilder-action-scaffolder/ │ ├── implementation-template.md ← Structured delivery artifact template │ ├── runtime-reference.md ← Action structure, params, response formats, SDKs, CLI │ └── action-patterns.md ← 12 complete action patterns with manifest + code -├── assets/ ← 10 JavaScript code templates +├── assets/ ← 9 JavaScript code templates │ ├── action-scaffold-template.js ← Minimal scaffold for quick prototyping │ ├── action-boilerplate.js ← Production-ready with logging and error handling │ ├── database-action-template.js ← Database CRUD with @adobe/aio-lib-db @@ -30,8 +30,7 @@ appbuilder-action-scaffolder/ │ ├── journaling-consumer-template.js ← Scheduled journal poller │ ├── large-payload-template.js ← Files SDK redirect for oversized responses │ ├── action-sequence-template.js ← Linear action sequence pipeline -│ ├── asset-compute-worker-template.js ← AEM rendition processing worker -│ └── contenthub-action-template.js ← Content Hub web action (asset IDs → AEM Assets Author API → panel) +│ └── asset-compute-worker-template.js ← AEM rendition processing worker └── evals/ └── evals.json ← 11 evaluation test cases ``` @@ -76,7 +75,6 @@ The skill follows a 6-step workflow (detailed in `references/playbook.md`): | Response exceeds 1 MB | large-payload-template.js | | Multi-action linear pipeline | action-sequence-template.js | | AEM rendition processing | asset-compute-worker-template.js | -| Content Hub web action (asset IDs → AEM Assets Author API → panel) | contenthub-action-template.js | ### Validate manifest before deploy diff --git a/plugins/app-builder/skills/appbuilder-action-scaffolder/SKILL.md b/plugins/app-builder/skills/appbuilder-action-scaffolder/SKILL.md index 1dcb29be..ba604e76 100644 --- a/plugins/app-builder/skills/appbuilder-action-scaffolder/SKILL.md +++ b/plugins/app-builder/skills/appbuilder-action-scaffolder/SKILL.md @@ -1,6 +1,6 @@ --- name: appbuilder-action-scaffolder -description: Create, implement, deploy, and debug Adobe Runtime actions with consistent layout, validation, and error handling. Use this skill whenever the user needs to add actions to an App Builder project, understand action structure (params, response format, web/raw actions), configure actions in the manifest, use App Builder SDKs (State, Files, Events, database), deploy and invoke actions via CLI, debug action issues, or implement patterns such as webhook receivers, custom event providers, journaling consumers, large payload redirects, action sequence pipelines, Asset Compute workers, and Content Hub web actions (the actions/generic/index.js that receives asset IDs from panels or modals, calls the AEM Assets Author API, and returns data to the Content Hub UI). Also trigger when users mention serverless functions in Adobe context, action logging, IMS authentication for actions, cron-style scheduled actions, or a Content Hub web action calling the AEM API. +description: Create, implement, deploy, and debug Adobe Runtime actions with consistent layout, validation, and error handling. Use this skill whenever the user needs to add actions to an App Builder project, understand action structure (params, response format, web/raw actions), configure actions in the manifest, use App Builder SDKs (State, Files, Events, database), deploy and invoke actions via CLI, debug action issues, or implement patterns such as webhook receivers, custom event providers, journaling consumers, large payload redirects, action sequence pipelines, and Asset Compute workers. Also trigger when users mention serverless functions in Adobe context, action logging, IMS authentication for actions, or cron-style scheduled actions. metadata: category: action-lifecycle license: Apache-2.0 @@ -26,7 +26,6 @@ Pick the template that matches the user's intent. Default to `assets/action-boil | Large payload response (>1 MB) | assets/large-payload-template.js | | Action sequence pipeline | assets/action-sequence-template.js | | Asset Compute worker (AEM renditions) | assets/asset-compute-worker-template.js | -| Content Hub web action (asset IDs → AEM Assets Author API → panel) | assets/contenthub-action-template.js | | Debug Runtime action issues | references/debugging.md | ## Fast Path (for clear requests) @@ -105,7 +104,7 @@ If there is any ambiguity — multiple patterns could fit, constraints are uncle - Use `references/runtime-reference.md` for action structure, params, response formats, SDK usage, and CLI operations. - Use `references/aem-apis.md` for AEM Content Fragment API surfaces — decision table, Delivery OpenAPI, Management OpenAPI, GraphQL persisted queries, and the deprecated Assets HTTP API — with auth patterns and action code for each. - Use `references/action-patterns.md` for common action patterns, including CRUD API, cron, multi-step processing, database CRUD, webhook intake, custom event providers, journaling consumers, large payload redirects, action sequence composition, and Asset Compute workers. -- Use `assets/database-action-template.js`, `assets/event-webhook-template.js`, `assets/event-provider-template.js`, `assets/journaling-consumer-template.js`, `assets/large-payload-template.js`, `assets/action-sequence-template.js`, and `assets/asset-compute-worker-template.js` when the user request maps directly to one of the newer boilerplate patterns. +- Use `assets/database-action-template.js`, `assets/event-webhook-template.js`, `assets/event-provider-template.js`, `assets/journaling-consumer-template.js`, `assets/large-payload-template.js`, `assets/action-sequence-template.js`, `assets/asset-compute-worker-template.js` when the user request maps directly to one of the newer boilerplate patterns. - Use `../_shared/categories/architecture-runtime.md` for Adobe service-specific guidance. ## Common Manifest Guardrail @@ -139,7 +138,6 @@ If there is any ambiguity — multiple patterns could fit, constraints are uncle - `assets/large-payload-template.js` — large payload response starter that writes oversized responses to Files storage and returns a redirect URL. - `assets/action-sequence-template.js` — Starter manifest and action layout for a linear action sequence pipeline. - `assets/asset-compute-worker-template.js` — Asset Compute worker scaffold for AEM rendition processing with the Asset Compute SDK. -- `assets/contenthub-action-template.js` — Content Hub web action starter that receives asset ID(s) plus auth context from a Content Hub panel/card/bulk action, calls the AEM Assets Author API server-side, and returns the result to the panel. ## Agent Integration diff --git a/plugins/app-builder/skills/appbuilder-action-scaffolder/assets/contenthub-action-template.js b/plugins/app-builder/skills/appbuilder-action-scaffolder/assets/contenthub-action-template.js deleted file mode 100644 index a823dd68..00000000 --- a/plugins/app-builder/skills/appbuilder-action-scaffolder/assets/contenthub-action-template.js +++ /dev/null @@ -1,71 +0,0 @@ -const { Core } = require('@adobe/aio-sdk'); - -/** - * Content Hub web action. - * - * Data flow: a Content Hub panel / card action / bulk action passes the selected - * asset ID(s) plus auth context to this action, which calls the AEM Assets Author - * API server-side (CORS blocks the browser from calling AEM directly) and returns - * the result to the panel to render. - * - * Params passed from the extension (see contenthub-extensions.md): - * assetId — asset URN from host.assetDetails.getCurrentAsset() (or assetIds[] for bulk) - * aemHost — AEM author host from host.discovery.getAemHost() - * apiKey — from host.auth.getApiKey() (never hardcode) - * imsOrg — from host.auth.getIMSInfo() - * - * For authenticated AEM API calls, set require-adobe-auth: true in ext.config.yaml, - * send the Authorization header from the panel, and read the token below. - */ -async function main(params) { - const logger = Core.Logger('main', { level: params.LOG_LEVEL || 'info' }); - - try { - logger.info('Content Hub action invoked', JSON.stringify({ assetId: params.assetId })); - - // Input validation - const requiredParams = ['assetId', 'aemHost']; - const missingParams = requiredParams.filter(p => !params[p]); - if (missingParams.length > 0) { - return { - statusCode: 400, - body: { error: `Missing required parameters: ${missingParams.join(', ')}` } - }; - } - - const { assetId, aemHost, apiKey, imsOrg } = params; - // When require-adobe-auth: true, the gateway injects the bearer token here: - const token = params.__ow_headers?.authorization?.substring(7); - - const response = await fetch(`https://${aemHost}/adobe/assets/${assetId}/metadata`, { - headers: { - 'Authorization': `Bearer ${token}`, - 'X-Api-Key': apiKey, // always from the frontend — never hardcode - 'x-gw-ims-org-id': imsOrg, - 'Content-Type': 'application/json' - } - }); - - if (!response.ok) { - throw new Error(`AEM Assets Author API failed (${response.status})`); - } - - const data = await response.json(); - const metadata = data.value ?? data; - - logger.info('Content Hub action completed successfully'); - return { - statusCode: 200, - headers: { 'Content-Type': 'application/json' }, - body: { metadata } - }; - } catch (error) { - logger.error('Content Hub action failed:', error.message); - return { - statusCode: 500, - body: { error: error.message } - }; - } -} - -exports.main = main; diff --git a/plugins/app-builder/skills/appbuilder-project-init/SKILL.md b/plugins/app-builder/skills/appbuilder-project-init/SKILL.md index 39b3d24c..31534b38 100644 --- a/plugins/app-builder/skills/appbuilder-project-init/SKILL.md +++ b/plugins/app-builder/skills/appbuilder-project-init/SKILL.md @@ -1,6 +1,6 @@ --- name: appbuilder-project-init -description: Initialize or scaffold an Adobe App Builder project end-to-end without Developer Console UI clicks. Creates the Console project and workspace, subscribes APIs, maps user intent to the right template, and runs non-interactive `aio app init`. Use whenever the user mentions creating an App Builder app, scaffolding a project, `aio app init`, setting up an Experience Cloud extension, adding actions or APIs, or bootstrapping App Builder — even if they don't say "App Builder". Also for SPA templates, AEM extensions, API Mesh, Asset Compute workers, and MCP projects. Also scaffolds a Content Hub extension (`aem/assets/contenthub/1`) from scratch — Console setup, file generation, build, dev server, deploy — when the user says "scaffold a Content Hub extension" or names a Content Hub surface (asset details panel, card action, bulk action, add assets wizard). Also handles debugging init failures — `aio app init` hangs, Node mismatches, npm failures, `aio login` issues, or `aio console` project/workspace/API errors. +description: Initialize or scaffold an Adobe App Builder project end-to-end without Developer Console UI clicks. Creates the Console project and workspace, subscribes APIs, maps user intent to the right template, and runs non-interactive `aio app init`. Use whenever the user mentions creating an App Builder app, scaffolding a project, `aio app init`, setting up an Experience Cloud extension, adding actions or APIs, or bootstrapping App Builder — even if they don't say "App Builder". Also for SPA templates, AEM extensions, API Mesh, Asset Compute workers, and MCP projects. Also scaffolds a Content Hub extension (`aem/assets/contenthub/1`) from scratch — Console setup, file generation, build, dev server, deploy — when the user says "scaffold a Content Hub extension" or names a Content Hub surface (asset details panel, card action, bulk action). Also handles debugging init failures — `aio app init` hangs, Node mismatches, npm failures, `aio login` issues, or `aio console` project/workspace/API errors. metadata: category: project-initialization license: Apache-2.0 @@ -143,7 +143,7 @@ Pick the template that matches the user's intent. When unclear, default to `@ado | User wants | Template | | --- | --- | | SPA with actions + React UI | @adobe/generator-app-excshell | -| Content Hub extension (asset details panel, card action, bulk action, add assets wizard) | Content Hub Scaffolding — see section below | +| Content Hub extension (asset details panel, card action, bulk action) | Content Hub Scaffolding — see section below | | AEM Content Fragment Console extension | @adobe/aem-cf-admin-ui-ext-tpl | | AEM React SPA (WKND-based) | @adobe/generator-app-aem-react | | Adobe API Mesh (GraphQL) | @adobe/generator-app-api-mesh | @@ -155,11 +155,11 @@ For a headless/backend-only request, prefer `init-bare` when possible. If the us ## Content Hub Extension Scaffolding -For a Content Hub extension (`aem/assets/contenthub/1` — asset details panels, card actions, bulk actions, add assets wizard) this skill runs a dedicated end-to-end workflow instead of a generator template: name → namespace selection → Console setup → file generation → npm install → `aio app use` → build → dev server → cert acceptance → open in Content Hub → deploy. +For a Content Hub extension (`aem/assets/contenthub/1` — asset details panels, card actions, bulk actions) this skill runs a dedicated end-to-end workflow instead of a generator template: name → namespace selection → Console setup → file generation → npm install → `aio app use` → build → dev server → cert acceptance → open in Content Hub → deploy. Read [`references/contenthub-scaffolding.md`](references/contenthub-scaffolding.md) for the full step-by-step workflow and [`references/contenthub-templates.md`](references/contenthub-templates.md) for the scaffold file templates. The workflow's final step does a manual first `aio app deploy`; for the full deployment story (Stage → Production, Extension Manager approval, CI/CD) chain to the `appbuilder-cicd-pipeline` skill. -**When it triggers:** the user says "create/scaffold a Content Hub extension", or names a Content Hub surface ("asset details panel", "card action button", "bulk action", "add assets wizard"). Once the extension is scaffolded and running, chain to `appbuilder-ui-scaffolder` for UI customization (React Spectrum patterns for each namespace). +**When it triggers:** the user says "create/scaffold a Content Hub extension", or names a Content Hub surface ("asset details panel", "card action button", "bulk action"). Once the extension is scaffolded and running, chain to `appbuilder-ui-scaffolder` for UI customization (React Spectrum patterns for each namespace). ## Initialize via Script @@ -314,6 +314,6 @@ After initialization, hand off to: - [references/templates.md](references/templates.md) — Template catalog with intent mapping and per-template post-init guidance - [references/debugging.md](references/debugging.md) — Troubleshooting guide for init failures, Node/npm issues, login problems, and first-run errors - [references/contenthub-scaffolding.md](references/contenthub-scaffolding.md) — Full Content Hub extension scaffolding workflow (Steps 1–17: Console setup, namespace selection, file generation, build, dev server, cert acceptance, deploy) -- [references/contenthub-templates.md](references/contenthub-templates.md) — All Content Hub scaffold file templates (`app.config.yaml`, `ext.config.yaml`, `ExtensionRegistration.js`, `App.js`, per-namespace components, `actions/generic/index.js`) +- [references/contenthub-templates.md](references/contenthub-templates.md) — All Content Hub scaffold file templates (`app.config.yaml`, `ext.config.yaml`, `ExtensionRegistration.js`, `App.js`, per-namespace components (`PanelAssetDetailsExtensionTab.js`, `CardActionModal.js`, `SelectionBarModal.js`), `actions/generic/index.js`) For deploying a Content Hub extension (Stage → Production, CDN URL, Extension Manager approval, CI/CD), use the `appbuilder-cicd-pipeline` skill — see its `references/contenthub-deploy.md`. \ No newline at end of file diff --git a/plugins/app-builder/skills/appbuilder-project-init/references/contenthub-scaffolding.md b/plugins/app-builder/skills/appbuilder-project-init/references/contenthub-scaffolding.md index 86801818..a6537c49 100644 --- a/plugins/app-builder/skills/appbuilder-project-init/references/contenthub-scaffolding.md +++ b/plugins/app-builder/skills/appbuilder-project-init/references/contenthub-scaffolding.md @@ -4,7 +4,7 @@ Complete step-by-step workflow for scaffolding a **Content Hub** App Builder ext Read this file when `appbuilder-project-init` is asked to create/scaffold a Content Hub extension. -> **When to use this:** the user wants to create a NEW Content Hub extension — tab panels in Asset Details, card/collection-tile action buttons, bulk-action-bar buttons, or Add Assets wizard panels. This drives the whole lifecycle from one prompt to a running, open browser tab. If the user already has a scaffolded project and only wants to customize the UI, use `appbuilder-ui-scaffolder` (§ Content Hub in `references/aem-extensions.md`) instead. +> **When to use this:** the user wants to create a NEW Content Hub extension — tab panels in Asset Details, card/collection-tile action buttons, or bulk-action-bar buttons. This drives the whole lifecycle from one prompt to a running, open browser tab. If the user already has a scaffolded project and only wants to customize the UI, use `appbuilder-ui-scaffolder` (§ Content Hub in `references/aem-extensions.md`) instead. **Fixed facts for this surface** (used by every step below): @@ -14,7 +14,7 @@ Read this file when `appbuilder-project-init` is asked to create/scaffold a Cont | Source dir | `src/aem-assets-contenthub-1/` | | SDK | `@adobe/uix-guest` | | Templates | [`references/contenthub-templates.md`](contenthub-templates.md) | -| Namespaces | `assetDetails` (tab panels), `card` (asset-card + collection-tile buttons), `selectionBar` (bulk-action bar), `addAssets` (wizard panels + `beforeUpload`/`onUploadComplete` hooks) | +| Namespaces | `assetDetails` (tab panels), `card` (asset-card + collection-tile buttons), `selectionBar` (bulk-action bar) | | Local dev open URL | `https://experience.adobe.com/?devMode=true&ext=https://localhost:9080#/assets/contenthub/` | Use `AskUserQuestion` for every user decision. Never print a test URL without opening it. Never ask the user to type anything (except when they choose "Other"). @@ -59,7 +59,6 @@ The user can select "Other" to type their own kebab-case name. Store as `extensi - "asset card" / "card action" / "buttons on cards" → `["card"]` - "asset details panel" / "tab panel in asset details" → `["assetDetails"]` - "bulk action" / "selection bar" → `["selectionBar"]` -- "Add Assets wizard" / "before upload hook" / "hydration panel" → `["addAssets"]` - "card and bulk actions" → `["card", "selectionBar"]` **Only ask when the prompt is generic** ("create a Content Hub extension" with no namespace hint). Use `AskUserQuestion` with `multiSelect: true`: @@ -73,11 +72,9 @@ options: description: "Buttons on asset card menus (Assets grid, inside a collection, link share) and on collection tiles — card namespace. onActionClick(resourceType, buttonId, resourceId, actionContext)." - label: "Selection bar (bulk action)" description: "Buttons in the bulk-action bar shown when assets are selected — selectionBar namespace. onActionClick(buttonId, assetIds[])." - - label: "Add Assets wizard" - description: "Panels before/after the Upload step, gate/enrich metadata (beforeUpload), react after upload (onUploadComplete) — addAssets namespace." ``` -Store the selected keys as `namespaces` (at least one). Notes: `card`/`selectionBar` open a modal on click via `host.modal.openDialog()`, so `ExtensionRegistration.js` uses `let guestConnection` (not `const`). `addAssets` panels use `postMessage` for step readiness — never call `openDialog` from a wizard panel. Step 10 uses `namespaces` to decide which component files to write. +Store the selected keys as `namespaces` (at least one). Notes: `card`/`selectionBar` open a modal on click via `host.modal.openDialog()`, so `ExtensionRegistration.js` uses `let guestConnection` (not `const`). Step 10 uses `namespaces` to decide which component files to write. ### Step 3 — Workspace @@ -186,7 +183,7 @@ Read [`references/contenthub-templates.md`](contenthub-templates.md) and write a - `src/aem-assets-contenthub-1/web-src/index.html`, `web-src/src/index.js`, `index.css`, `config.json` - `web-src/src/components/Constants.js`, `App.js`, `ExtensionRegistration.js` - `src/aem-assets-contenthub-1/actions/utils.js`, `actions/generic/index.js` -- Namespace components — write **only** for namespaces selected in Step 2: `TabPanel.js` (assetDetails), `CardActionModal.js` (card), `SelectionBarModal.js` (selectionBar), `AddAssetsPanel.js` (addAssets) +- Namespace components — write **only** for namespaces selected in Step 2: `PanelAssetDetailsExtensionTab.js` (assetDetails), `CardActionModal.js` (card), `SelectionBarModal.js` (selectionBar) Keep `App.js` routes and the `ExtensionRegistration.js` `methods` object limited to the selected namespaces. @@ -285,10 +282,9 @@ All UI files are under `src/aem-assets-contenthub-1/web-src/src/components/` | What you want to change | File to edit | |---|---| | Which panels / buttons appear, their title, icon, or label | `ExtensionRegistration.js` | -| Asset Details panel content | `TabPanel.js` ← only if assetDetails selected | +| Asset Details panel content | `PanelAssetDetailsExtensionTab.js` ← only if assetDetails selected | | Card action modal content | `CardActionModal.js` ← only if card selected | | Selection bar (bulk action) content | `SelectionBarModal.js` ← only if selectionBar selected | -| Add Assets wizard panel content | `AddAssetsPanel.js` ← only if addAssets selected | | Server-side logic / AEM API calls | `actions/generic/index.js` | ``` diff --git a/plugins/app-builder/skills/appbuilder-project-init/references/contenthub-templates.md b/plugins/app-builder/skills/appbuilder-project-init/references/contenthub-templates.md index b170be47..6a00e575 100644 --- a/plugins/app-builder/skills/appbuilder-project-init/references/contenthub-templates.md +++ b/plugins/app-builder/skills/appbuilder-project-init/references/contenthub-templates.md @@ -8,7 +8,7 @@ Complete templates for every Content Hub extension scaffold file. Before writing Extension point used throughout: `aem/assets/contenthub/1` Source directory: `src/aem-assets-contenthub-1/` -> Only write the namespace components you need — `TabPanel.js` (assetDetails), `CardActionModal.js` (card), `SelectionBarModal.js` (selectionBar), `AddAssetsPanel.js` (addAssets). The shared skeleton (`package.json`, `index.html`, `index.js`, `index.css`, `Constants.js`, `App.js`, `actions/*`, `hooks/post-deploy.js`, `.eslintrc.js`) is always written. +> Only write the namespace components you need — `PanelAssetDetailsExtensionTab.js` (assetDetails), `CardActionModal.js` (card), `SelectionBarModal.js` (selectionBar). The shared skeleton (`package.json`, `index.html`, `index.js`, `index.css`, `Constants.js`, `App.js`, `actions/*`, `hooks/post-deploy.js`, `.eslintrc.js`) is always written. --- @@ -150,7 +150,6 @@ module.exports = { - `assetDetails` — tab panels in the Asset Details Dialog side rail - `card` — action buttons on asset cards (Assets grid, collection, link share) and on collection tiles in the Collections grid - `selectionBar` — bulk action buttons in the selection bar (multi-select) -- `addAssets` — wizard panels before/after the Upload step; `beforeUpload` hook to gate/enrich uploads; `onUploadComplete` hook after upload finishes (Remove entries for namespaces not wired in ExtensionRegistration.js) @@ -179,7 +178,7 @@ First time: accept the self-signed cert at https://localhost:9080. ## Key Files to Customize - `ExtensionRegistration.js` — declares all namespaces. Uses `let guestConnection` so `onActionClick` can call `host.modal.openDialog()` after `register()` resolves. -- `TabPanel.js` — panel UI for `assetDetails` namespace (rendered in iframe) +- `PanelAssetDetailsExtensionTab.js` — panel UI for `assetDetails` namespace (rendered in iframe) - `CardActionModal.js` — modal UI for `card` namespace; reads its data from the `contentUrl` query (`URLSearchParams`), NOT `getPayload()` - `SelectionBarModal.js` — modal UI for `selectionBar` namespace; reads `assetIds[]` from the `contentUrl` query (`URLSearchParams` + `JSON.parse`), NOT `getPayload()` - `actions/generic/index.js` — server-side logic for AEM API calls @@ -327,7 +326,7 @@ This file is overwritten by `aio app run` (localhost URL) and `aio app deploy` ( export const extensionId = 'sample-extension'; ``` -The `extensionId` **must** be identical in `register()` (ExtensionRegistration.js) and `attach()` (TabPanel.js). Both import from this file. +The `extensionId` **must** be identical in `register()` (ExtensionRegistration.js) and `attach()` (PanelAssetDetailsExtensionTab.js). Both import from this file. --- @@ -340,10 +339,9 @@ import React from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { HashRouter as Router, Routes, Route } from 'react-router-dom'; import ExtensionRegistration from './ExtensionRegistration'; -import TabPanel from './TabPanel'; // assetDetails namespace — remove if not selected +import PanelAssetDetailsExtensionTab from './PanelAssetDetailsExtensionTab'; // assetDetails namespace — remove if not selected import CardActionModal from './CardActionModal'; // card namespace — remove if not selected import SelectionBarModal from './SelectionBarModal'; // selectionBar namespace — remove if not selected -import AddAssetsPanel from './AddAssetsPanel'; // addAssets namespace — remove if not selected function App() { return ( @@ -353,14 +351,11 @@ function App() { } /> } /> {/* assetDetails namespace — remove if not selected */} - } /> + } /> {/* card namespace — remove if not selected */} } /> {/* selectionBar namespace — remove if not selected */} } /> - {/* addAssets namespace — remove both routes if not selected */} - } /> - } /> @@ -389,7 +384,7 @@ export default App; ## `src/aem-assets-contenthub-1/web-src/src/components/ExtensionRegistration.js` -Contains all four Content Hub namespaces. **When scaffolding, include only the namespace blocks the user selected in Step 2.** Each block is clearly marked — remove unused ones. `card` and `selectionBar` require `let guestConnection` (not `const`) because `onActionClick` is called after `register()` resolves. `addAssets` does NOT need `guestConnection` — its lifecycle hooks run synchronously without calling host APIs. +Contains all three Content Hub namespaces. **When scaffolding, include only the namespace blocks the user selected in Step 2.** Each block is clearly marked — remove unused ones. `card` and `selectionBar` require `let guestConnection` (not `const`) because `onActionClick` is called after `register()` resolves. ```js import React from 'react'; @@ -436,7 +431,7 @@ function ExtensionRegistration() { title: '{{DISPLAY_NAME}}', tooltip: '{{DISPLAY_NAME}}', icon: 'Extension', // React-Spectrum workflow icon name - contentUrl: '/#tab-panel', // must match a in App.js + contentUrl: '/#asset-details-extension-tab', // must match a in App.js }, ]; }, @@ -512,49 +507,6 @@ function ExtensionRegistration() { }, }, - // ── ADD ASSETS NAMESPACE ───────────────────────────────────────────── - // Extends the "Add Assets" (hydration) flow with wizard panels and lifecycle hooks. - // Remove this block if addAssets was not selected. - // - // getPanels() returns panels injected before ('pre') or after ('post') the Upload step. - // 'pre' panels gate the Next button — the panel must send a postMessage to unlock it. - // 'post' panels are always ready (no signal required). - // - // beforeUpload(ctx) is called before upload starts: - // ctx: { files, metadata, uploadPath } - // return { proceed: true, metadata } to pass through (optionally merging extra metadata) - // return { proceed: false, message: '...' } to block the upload with a user-facing message - // - // onUploadComplete(ctx) fires after upload finishes (parallel, fire-and-forget). - addAssets: { - getPanels() { - return [ - { - id: '{{EXTENSION_NAME}}-pre-step', - title: '{{DISPLAY_NAME}} Pre-Upload', - position: 'pre', // shown BEFORE the Upload step - contentUrl: '/#pre-step', // must match a in App.js - }, - // Add more panels here, or remove this array entry if you only need post panels. - { - id: '{{EXTENSION_NAME}}-post-step', - title: '{{DISPLAY_NAME}} Post-Upload', - position: 'post', // shown AFTER the Upload step - contentUrl: '/#post-step', - }, - ]; - }, - async beforeUpload({ files, metadata, uploadPath }) { - // Return { proceed: true, metadata } to continue (optionally inject extra metadata). - // Return { proceed: false, message: '...' } to block with a user-facing error. - return { proceed: true, metadata }; - }, - async onUploadComplete({ files, metadata, uploadPath }) { - // Fires after all files finish uploading (fire-and-forget, return value ignored). - console.log('{{EXTENSION_NAME}} onUploadComplete', { fileCount: files.length, uploadPath }); - }, - }, - }, }); }; @@ -579,7 +531,7 @@ export default ExtensionRegistration; --- -## `src/aem-assets-contenthub-1/web-src/src/components/TabPanel.js` +## `src/aem-assets-contenthub-1/web-src/src/components/PanelAssetDetailsExtensionTab.js` This is the panel content rendered inside Content Hub's iframe. All Host API calls go through `guestConnection.host`. @@ -599,7 +551,7 @@ import { import { extensionId } from './Constants'; import actions from '../config.json'; -export default function TabPanel() { +export default function PanelAssetDetailsExtensionTab() { const [guestConnection, setGuestConnection] = useState(null); const [asset, setAsset] = useState(null); const [loading, setLoading] = useState(true); @@ -882,112 +834,6 @@ export default function SelectionBarModal() { --- -## `src/aem-assets-contenthub-1/web-src/src/components/AddAssetsPanel.js` - -Wizard panel rendered inside a `GuestUIFrame` for the `addAssets` namespace. Used for both `pre` and `post` position panels (same component, different route). - -**Critical:** `getPanelId()` strips the leading `#` (and optional `/`) from `window.location.hash`. -`contentUrl: '/#pre-step'` loads as hash `#pre-step` (NOT `#/pre-step`) — using `.replace('#/', '')` silently fails and sends the wrong `panelId` in the `postMessage`, keeping Next permanently disabled. -Always use the regex `/^#\/?/` as shown below. - -```js -import React, { useState, useEffect } from 'react'; -import { attach } from '@adobe/uix-guest'; -import { - Provider, - defaultTheme, - View, - Heading, - Text, - Button, - ProgressCircle, -} from '@adobe/react-spectrum'; -import { extensionId } from './Constants'; - -// contentUrl '/#pre-step' loads with hash '#pre-step' (no slash after #). -// Use /^#\/?/ to strip '#' or '#/' — plain '#/' replace silently fails and breaks the panelId. -function getPanelId() { - const route = window.location.hash.replace(/^#\/?/, '').split('?')[0]; - return `{{EXTENSION_NAME}}-${route}`; -} - -function isPrePanel() { - return window.location.hash.includes('pre-step'); -} - -export default function AddAssetsPanel() { - const [guestConnection, setGuestConnection] = useState(null); - const [ready, setReady] = useState(false); - const panelId = getPanelId(); - const isPre = isPrePanel(); - - useEffect(() => { - (async () => { - const connection = await attach({ id: extensionId }); - setGuestConnection(connection); - // Post panels default to ready (no signalling required). - if (!isPre) setReady(true); - })(); - }, []); - - function markReady() { - // Signal the wizard host: Next button becomes clickable for this panel. - // panelId must exactly match the id returned by getPanels() in ExtensionRegistration.js. - window.parent.postMessage( - { type: 'addAssets:setReadyToAdvance', panelId, ready: true }, - '*' - ); - setReady(true); - } - - if (!guestConnection) { - return ( - - - - - - - - ); - } - - return ( - - - - {{DISPLAY_NAME}} — {isPre ? 'Pre-Upload' : 'Post-Upload'} - - - - - {isPre - ? 'Complete any required steps before uploading your assets, then click "Ready to Upload" to proceed.' - : 'Your assets have been uploaded. Review or tag them below.'} - - - - {/* Add your custom wizard panel UI here */} - - {isPre && !ready && ( - - )} - - {ready && ( - - ✓ {isPre ? 'Ready to proceed to upload.' : 'Upload complete.'} - - )} - - - ); -} -``` - ---- - ## `src/aem-assets-contenthub-1/actions/utils.js` ```js @@ -1026,7 +872,7 @@ const { errorResponse, getBearerToken, checkMissingRequestInputs } = require('.. /** * Generic Web Action — {{DISPLAY_NAME}} * - * Called from TabPanel.js with the current asset ID. + * Called from PanelAssetDetailsExtensionTab.js with the current asset ID. * Customize this to call AEM Assets Author API. * * Params passed from the panel: @@ -1098,7 +944,7 @@ Content Hub UI extension for the `aem/assets/contenthub/1` extension point. src/aem-assets-contenthub-1/ web-src/src/components/ ExtensionRegistration.js — registers tab panels with Content Hub - TabPanel.js — custom panel UI (rendered in iframe) + PanelAssetDetailsExtensionTab.js — custom panel UI (rendered in iframe) App.js — React routing Constants.js — extensionId (shared between register + attach) actions/ @@ -1158,7 +1004,7 @@ getTabPanels() { title: '{{DISPLAY_NAME}}', tooltip: '{{DISPLAY_NAME}}', icon: 'Extension', - contentUrl: '/#tab-panel', + contentUrl: '/#asset-details-extension-tab', }, { id: '{{EXTENSION_NAME}}-second-panel', @@ -1179,4 +1025,4 @@ import SecondPanel from './SecondPanel'; // create this component } /> ``` -### Create `SecondPanel.js` — copy the `TabPanel.js` template, rename the component and customize the UI. +### Create `SecondPanel.js` — copy the `PanelAssetDetailsExtensionTab.js` template, rename the component and customize the UI. diff --git a/plugins/app-builder/skills/appbuilder-testing/SKILL.md b/plugins/app-builder/skills/appbuilder-testing/SKILL.md index 354a411f..0abeb72a 100644 --- a/plugins/app-builder/skills/appbuilder-testing/SKILL.md +++ b/plugins/app-builder/skills/appbuilder-testing/SKILL.md @@ -5,7 +5,7 @@ description: >- integration tests against deployed actions, contract tests for Adobe API interactions, and React component tests using Testing Library. Provides mock helpers for State, Files, Events SDKs, @adobe/aio-lib-* clients, ExC Shell context (@adobe/exc-app), and UIX Guest SDK (@adobe/uix-guest) - for AEM and Content Hub extension components (ExtensionRegistration, TabPanel, CardActionModal, + for AEM and Content Hub extension components (ExtensionRegistration, PanelAssetDetailsExtensionTab, CardActionModal, SelectionBarModal). Use this skill whenever the user mentions testing App Builder actions, writing unit tests for Runtime actions, creating integration tests, mocking Adobe SDKs, setting up test fixtures, running aio app test, or wants to verify action behavior before deployment. Also trigger @@ -41,7 +41,7 @@ Pick the template or reference that matches the user's intent. Default to `asset | Contract test for API interactions | references/testing-patterns.md | — | | Pre-deployment verification | references/checklist.md | — | | Debug test failures | references/debugging.md | — | -| Content Hub extension test (ExtensionRegistration, TabPanel, card/selection-bar modals) | references/testing-patterns.md | assets/uix-guest-mock-helper.js | +| Content Hub extension test (ExtensionRegistration, PanelAssetDetailsExtensionTab, card/selection-bar modals) | references/testing-patterns.md | assets/uix-guest-mock-helper.js | ## Fast Path (for clear requests) diff --git a/plugins/app-builder/skills/appbuilder-ui-scaffolder/SKILL.md b/plugins/app-builder/skills/appbuilder-ui-scaffolder/SKILL.md index 6e75d0f7..f605a9cd 100644 --- a/plugins/app-builder/skills/appbuilder-ui-scaffolder/SKILL.md +++ b/plugins/app-builder/skills/appbuilder-ui-scaffolder/SKILL.md @@ -6,11 +6,11 @@ description: >- Guides ExC Shell integration with @adobe/exc-app including runtime.done(), IMS token passthrough, and shell theming. Guides AEM UI Extension development with @adobe/uix-guest for Content Fragment Console, CF Editor, Universal Editor, Assets View, and Content Hub surfaces (Content Hub via - aem/assets/contenthub/1 — asset details panels, card actions, bulk actions, Add Assets wizard). + aem/assets/contenthub/1 — asset details panels, card actions, bulk actions). Trigger on: building App Builder UI, React Spectrum components, ExC Shell pages, forms, data tables, dialogs, modals, navigation, theming, web-src, Spectrum, @adobe/exc-app, AEM extension, AEM UI extension, Content Fragment Console, Universal Editor extension, Content Hub panel, Content Hub - card action, Content Hub bulk action, add assets wizard, uix-guest, @adobe/uix-guest, extension points + card action, Content Hub bulk action, uix-guest, @adobe/uix-guest, extension points for AEM, customizing AEM surfaces. metadata: category: frontend @@ -36,7 +36,7 @@ Identify the user's intent, then read the referenced sections to generate tailor | ExC Shell setup | `references/shell-integration.md` | `@adobe/exc-app`, `Provider`, `defaultTheme` | | Connect UI to backend actions | `references/action-integration.md` | `fetch()` with IMS token | | AEM UI Extension (CF Console, CF Editor, Universal Editor, Assets View) | `references/aem-extensions.md` | `@adobe/uix-guest`, `register()`, `sharedContext` | -| Content Hub extension (panels, card actions, bulk actions, add assets wizard) | `references/aem-extensions.md` § Content Hub | `@adobe/uix-guest`, `register()`, `attach()`, `assetDetails`/`card`/`selectionBar`/`addAssets` namespaces, `host.modal.openDialog`, `postMessage` readiness signalling | +| Content Hub extension (panels, card actions, bulk actions) | `references/aem-extensions.md` § Content Hub | `@adobe/uix-guest`, `register()`, `attach()`, `assetDetails`/`card`/`selectionBar` namespaces, `host.modal.openDialog` | | Debug UI issues | `references/debugging.md` | Shell spinner, CORS, blank screen, auth | ## Fast Path (for clear requests) @@ -58,7 +58,7 @@ Examples of fast-path triggers: - "Build a Content Fragment Console extension" → Read `references/aem-extensions.md` § CF Console, generate directly - "Add a header menu button to the Universal Editor" → Read `references/aem-extensions.md` § Universal Editor, generate directly - "Create an AEM extension with uix-guest" → Read `references/aem-extensions.md` § Core Registration, generate directly -- "Add a Content Hub panel / card action / bulk action / add-assets step" → Read `references/aem-extensions.md` § Content Hub, generate directly +- "Add a Content Hub panel / card action / bulk action" → Read `references/aem-extensions.md` § Content Hub, generate directly - "Create / scaffold a new extension from scratch" → Redirect to the `appbuilder-project-init` skill (Console setup, template selection, file generation, dev server) If there is any ambiguity — multiple patterns could fit, constraints are unclear, or the user hasn't specified enough — fall through to the full workflow below. @@ -135,7 +135,7 @@ If there is any ambiguity — multiple patterns could fit, constraints are uncle - Use `references/routing-patterns.md` for SPA routing with React Router in ExC Shell. - Use `references/action-integration.md` for calling backend actions from the SPA. - Use `references/checklist.md` for pre-handoff UI quality validation. -- Use `references/aem-extensions.md` for AEM UI Extension patterns (`@adobe/uix-guest`) — Content Fragment Console/Editor, Universal Editor, Assets View, and Content Hub (`aem/assets/contenthub/1`: asset details tab panels, card/selection-bar/add-assets namespaces, Host APIs, web actions). +- Use `references/aem-extensions.md` for AEM UI Extension patterns (`@adobe/uix-guest`) — Content Fragment Console/Editor, Universal Editor, Assets View, and Content Hub (`aem/assets/contenthub/1`: asset details tab panels, card/selection-bar namespaces, Host APIs, web actions). - Use `references/debugging.md` for common SPA debugging scenarios (shell spinner, CORS, auth, blank screen, performance). ## Chaining diff --git a/plugins/app-builder/skills/appbuilder-ui-scaffolder/references/aem-extensions.md b/plugins/app-builder/skills/appbuilder-ui-scaffolder/references/aem-extensions.md index f506ae67..1cee38db 100644 --- a/plugins/app-builder/skills/appbuilder-ui-scaffolder/references/aem-extensions.md +++ b/plugins/app-builder/skills/appbuilder-ui-scaffolder/references/aem-extensions.md @@ -71,7 +71,7 @@ Extension point identifiers: - `aem/cf-editor/1` — Content Fragment Editor - `aem/universal-editor/1` — Universal Editor - `aem/assets/1` — Assets View (requires Assets Ultimate license) -- `aem/assets/contenthub/1` — Content Hub (unified — asset details, card, selection bar, and add-assets surfaces via one `register()` call) +- `aem/assets/contenthub/1` — Content Hub (unified — asset details, card, and selection bar surfaces via one `register()` call) --- @@ -415,18 +415,17 @@ Refer to [Assets View extension docs](https://developer.adobe.com/uix/docs/servi ## Content Hub Extensions (`aem/assets/contenthub/1`) -Content Hub is a single extension point that spans **four** surfaces, each opted into via a method namespace in one `register()` call: +Content Hub is a single extension point that spans **three** surfaces, each opted into via a method namespace in one `register()` call: - **Asset Details Dialog** (`assetDetails`) — custom tab panels in the side rail. - **Asset card actions** (`card`) — buttons on asset cards (Assets grid, inside a collection, link-share) **and** on collection tiles in the Collections grid; the surface is passed in `actionContext.context`. - **Selection bar / bulk actions** (`selectionBar`) — buttons in the multi-select action bar. -- **Add Assets wizard** (`addAssets`) — inject panels before/after the upload step, gate uploads, react to upload completion. > Use the deprecated ID `aem/contenthub/assets/details/1` only for older projects mid-transition; new projects use `aem/assets/contenthub/1`. **Auth is different from the other AEM surfaces:** Content Hub does *not* use `sharedContext`. Get auth and environment from the `host` namespaces instead (`host.auth.getIMSInfo()`, `host.discovery.getAemHost()` — see Host APIs below). -### Registration (all four namespaces) +### Registration (all three namespaces) ```js import { register } from '@adobe/uix-guest'; @@ -449,16 +448,11 @@ guestConnection = await register({ getActionButtons(actionContext) { /* buttons in the bulk-action bar */ }, async onActionClick(buttonId, assetIds) { /* … */ }, }, - addAssets: { - getPanels() { /* wizard panels before/after the Upload step */ }, - async beforeUpload(ctx) { /* gate or enrich metadata before upload */ }, - async onUploadComplete(ctx) { /* react after upload finishes */ }, - }, }, }); ``` -Opt into any combination — only implement the namespaces you use, and scaffold one component per namespace (`TabPanel.js` for `assetDetails`, `CardActionModal.js` for `card`, `SelectionBarModal.js` for `selectionBar`, `AddAssetsPanel.js` for `addAssets`). `card` and `selectionBar` are gated by the `EXTENSIBILITY_AEM_CONTENTHUB` feature flag; when it is off the host renders no buttons for those surfaces, but `assetDetails` panels still render. +Opt into any combination — only implement the namespaces you use, and scaffold one component per namespace (`PanelAssetDetailsExtensionTab.js` for `assetDetails`, `CardActionModal.js` for `card`, `SelectionBarModal.js` for `selectionBar`). `card` and `selectionBar` are gated by the `EXTENSIBILITY_AEM_CONTENTHUB` feature flag; when it is off the host renders no buttons for those surfaces, but `assetDetails` panels still render. `app.config.yaml` includes the unified extension point once: @@ -481,7 +475,7 @@ assetDetails: { title: 'My Panel', // panel header (Content Hub renders it) tooltip: 'My Panel', // side-rail icon tooltip icon: 'Extension', // React-Spectrum workflow icon name - contentUrl: '/#tab-panel', // hash route — must match a in App.js + contentUrl: '/#asset-details-extension-tab', // hash route — must match a in App.js }, ]; }, @@ -549,38 +543,6 @@ selectionBar: { The host prefixes selection-bar button ids internally (`ext::`); your code always uses the original `btn.id` — that's what `onActionClick` receives too. -### Add Assets Wizard (`addAssets`) - -Integrates with the "Add Assets" (hydration) flow: inject wizard steps, gate/enrich uploads, and react to completion. - -```js -addAssets: { - getPanels() { - return [ - { id: 'my-pre-step', title: 'Select Campaign', position: 'pre', contentUrl: '/#pre-step' }, - { id: 'my-post-step', title: 'Tag Assets', position: 'post', contentUrl: '/#post-step' }, - ]; - }, - // Return { proceed:false, message } to block, or { proceed:true, metadata } to merge metadata. - async beforeUpload({ files, metadata, uploadPath }) { - return { proceed: true, metadata: { ...metadata, 'xdm:campaignName': 'Q3 Launch' } }; - }, - // Fires after all files upload (parallel, fire-and-forget). Return value ignored. - async onUploadComplete({ files, metadata, uploadPath }) { /* e.g. post to a webhook */ }, -} -``` - -Step order: `[pre panels] → [Upload step] → [post panels]`. A `pre` panel blocks **Next** by default; its iframe must signal readiness via `postMessage` (there is no `host.modal` in a wizard panel): - -```js -window.parent.postMessage( - { type: 'addAssets:setReadyToAdvance', panelId: 'my-pre-step', ready: true }, - '*' -); -``` - -`post` panels default to ready. For `beforeUpload`, multiple extensions merge (last-writer-wins per key) and any single `{ proceed: false }` blocks the upload. - ### Opening a Modal (`modal`) Card and selection-bar actions have no panel of their own — they open a modal whose content is another hash route in the same guest app. **Content Hub's `openDialog` takes a single config object** — you never pass `{ id }` (the UIX host auto-injects the extension id on its side). This is the **opposite** of the other AEM surfaces, which use `host.modal.showUrl({ title, url })` + `close()`. Don't cross them. @@ -619,11 +581,9 @@ import { HashRouter as Router, Routes, Route } from 'react-router-dom'; } /> } /> - } /> {/* assetDetails */} + } /> {/* assetDetails */} } /> {/* card modal */} } /> {/* selectionBar modal */} - } /> {/* addAssets */} - } /> ``` @@ -646,7 +606,7 @@ const assetId = await guestConnection.host.assetDetails.getCurrentAsset(); // p Never call AEM APIs from the browser (CORS blocks them) — route through an App Builder web action: ```js -// In TabPanel.js +// In PanelAssetDetailsExtensionTab.js const { accessToken, imsOrg } = await guestConnection.host.auth.getIMSInfo(); const apiKey = await guestConnection.host.auth.getApiKey(); const aemHost = await guestConnection.host.discovery.getAemHost(); @@ -754,4 +714,4 @@ extensions: | CF Editor | `aem/cf-editor/1` | `headerMenu`, `rte` | `contentFragment`, `modal`, `toaster` | | Universal Editor | `aem/universal-editor/1` | `headerMenu` | `modal` | | Assets View | `aem/assets/1` | `actionBar`, `headerMenu` | `modal` | -| Content Hub | `aem/assets/contenthub/1` | `assetDetails`, `card`, `selectionBar`, `addAssets` | `auth`, `discovery`, `toast`, `i18n`, `modal` (`openDialog`/`closeDialog`), `assetDetails` | +| Content Hub | `aem/assets/contenthub/1` | `assetDetails`, `card`, `selectionBar` | `auth`, `discovery`, `toast`, `i18n`, `modal` (`openDialog`/`closeDialog`), `assetDetails` | From b2fedee6024c5c3d6af62c0a082c76dbc8f2a74f Mon Sep 17 00:00:00 2001 From: Naradasi Tejaswi Date: Fri, 10 Jul 2026 16:05:36 +0530 Subject: [PATCH 3/6] feat(contenthub): update skill --- .../app-builder/skills/appbuilder-cicd-pipeline/SKILL.md | 8 ++++---- .../app-builder/skills/appbuilder-e2e-testing/SKILL.md | 3 +-- plugins/app-builder/skills/appbuilder-testing/SKILL.md | 6 ++---- .../app-builder/skills/appbuilder-ui-scaffolder/SKILL.md | 2 +- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/plugins/app-builder/skills/appbuilder-cicd-pipeline/SKILL.md b/plugins/app-builder/skills/appbuilder-cicd-pipeline/SKILL.md index fb42221b..dae22b30 100644 --- a/plugins/app-builder/skills/appbuilder-cicd-pipeline/SKILL.md +++ b/plugins/app-builder/skills/appbuilder-cicd-pipeline/SKILL.md @@ -9,8 +9,8 @@ description: >- pipelines, continuous integration, continuous delivery, deploy automation, multi-environment promotion, aio app add ci, or wants to automate their App Builder build and release process. Also trigger when users mention deploy workflows, release pipelines, GitHub secrets for - App Builder, deploy a Content Hub extension, aio app deploy for AEM extensions, Extension - Manager approval, or automate deployment of a Content Hub or AEM UI extension. + App Builder, aio app deploy for AEM extensions, Extension + Manager approval, or automate deployment of an AEM UI extension. metadata: category: deployment-automation license: Apache-2.0 @@ -32,7 +32,7 @@ Set up CI/CD pipelines for Adobe App Builder projects — GitHub Actions (primar | Azure DevOps / GitLab CI / Jenkins | references/generic-pipeline-guide.md | | Secrets setup guide | references/secrets-management.md | | Debugging deploy failures | references/debugging.md | -| Content Hub extension deploy (manual `aio app deploy`, Stage→Prod, CDN URL, Extension Manager approval) | references/contenthub-deploy.md | +| Content Hub extension deploy | references/contenthub-deploy.md | ## Fast Path (for clear requests) @@ -95,7 +95,7 @@ If user specifies Azure DevOps, GitLab CI, or Jenkins → use `references/generi - Use `references/secrets-management.md` for OAuth S2S credential extraction and GitHub secrets setup. - Use `references/debugging.md` for troubleshooting deploy failures, CI errors, and workspace promotion issues. - Use `references/checklist.md` for pre-merge CI readiness validation. -- Use `references/contenthub-deploy.md` for manual Content Hub extension deployment (Stage → Production, CDN URL parsing, Extension Manager approval, re-deploy after code changes). +- Use `references/contenthub-deploy.md` for Content Hub extension deployment. - Use `assets/deploy-stage.yml`, `assets/deploy-prod.yml`, `assets/pr-test.yml` as workflow templates. - Use `assets/fetch-secrets.sh` to extract secret values from workspace configuration. - Official Adobe docs: [https://developer.adobe.com/app-builder/docs/guides/app_builder_guides/deployment/cicd-using-github-actions](https://developer.adobe.com/app-builder/docs/guides/app_builder_guides/deployment/cicd-using-github-actions) diff --git a/plugins/app-builder/skills/appbuilder-e2e-testing/SKILL.md b/plugins/app-builder/skills/appbuilder-e2e-testing/SKILL.md index 98f8004b..346b852f 100644 --- a/plugins/app-builder/skills/appbuilder-e2e-testing/SKILL.md +++ b/plugins/app-builder/skills/appbuilder-e2e-testing/SKILL.md @@ -5,7 +5,7 @@ description: >- application. Covers Playwright E2E testing for ExC Shell SPAs, AEM extension UIs (Content Fragment Console, Universal Editor, Content Hub), and full-stack flows. Use when the user mentions: "E2E test", "end-to-end test", "Playwright", "browser test", "test my SPA in the browser", "test my AEM - extension", "test my Content Hub extension", "test a Content Hub panel, card action, or bulk action", "test the + extension", "test the full flow", "integration test with UI", "headless browser test", "E2E in CI". This skill is for BROWSER-based testing only. For Jest unit tests of actions or React components, use appbuilder-testing instead. @@ -28,7 +28,6 @@ Identify the user's intent, then read the referenced sections to generate tailor | E2E test for ExC Shell SPA | `references/e2e-testing-patterns.md` | `assets/playwright.config.ts`, `assets/e2e-test-template.spec.ts` | | Test AEM extension in browser | `references/aem-extension-testing.md` | `assets/playwright.config.ts` | | E2E tests in CI pipeline | `references/ci-integration.md` | `assets/e2e-ci-workflow.yml` | -| Content Hub extension E2E (panel render, card action, bulk action) | `references/aem-extension-testing.md` | `assets/playwright.config.ts` | ## Fast Path (for clear requests) diff --git a/plugins/app-builder/skills/appbuilder-testing/SKILL.md b/plugins/app-builder/skills/appbuilder-testing/SKILL.md index 0abeb72a..9e85a8b9 100644 --- a/plugins/app-builder/skills/appbuilder-testing/SKILL.md +++ b/plugins/app-builder/skills/appbuilder-testing/SKILL.md @@ -5,12 +5,11 @@ description: >- integration tests against deployed actions, contract tests for Adobe API interactions, and React component tests using Testing Library. Provides mock helpers for State, Files, Events SDKs, @adobe/aio-lib-* clients, ExC Shell context (@adobe/exc-app), and UIX Guest SDK (@adobe/uix-guest) - for AEM and Content Hub extension components (ExtensionRegistration, PanelAssetDetailsExtensionTab, CardActionModal, - SelectionBarModal). Use this skill whenever the user mentions testing App Builder actions, writing + for AEM and Content Hub extension components. Use this skill whenever the user mentions testing App Builder actions, writing unit tests for Runtime actions, creating integration tests, mocking Adobe SDKs, setting up test fixtures, running aio app test, or wants to verify action behavior before deployment. Also trigger when users mention Jest configuration for App Builder, test coverage, CI test setup, React component - test, Testing Library, UI test, test my Content Hub extension, mock shell/extension context, debug + test, Testing Library, UI test, mock shell/extension context, debug test failures, or fix Jest errors. metadata: category: testing @@ -41,7 +40,6 @@ Pick the template or reference that matches the user's intent. Default to `asset | Contract test for API interactions | references/testing-patterns.md | — | | Pre-deployment verification | references/checklist.md | — | | Debug test failures | references/debugging.md | — | -| Content Hub extension test (ExtensionRegistration, PanelAssetDetailsExtensionTab, card/selection-bar modals) | references/testing-patterns.md | assets/uix-guest-mock-helper.js | ## Fast Path (for clear requests) diff --git a/plugins/app-builder/skills/appbuilder-ui-scaffolder/SKILL.md b/plugins/app-builder/skills/appbuilder-ui-scaffolder/SKILL.md index f605a9cd..19a6f68e 100644 --- a/plugins/app-builder/skills/appbuilder-ui-scaffolder/SKILL.md +++ b/plugins/app-builder/skills/appbuilder-ui-scaffolder/SKILL.md @@ -135,7 +135,7 @@ If there is any ambiguity — multiple patterns could fit, constraints are uncle - Use `references/routing-patterns.md` for SPA routing with React Router in ExC Shell. - Use `references/action-integration.md` for calling backend actions from the SPA. - Use `references/checklist.md` for pre-handoff UI quality validation. -- Use `references/aem-extensions.md` for AEM UI Extension patterns (`@adobe/uix-guest`) — Content Fragment Console/Editor, Universal Editor, Assets View, and Content Hub (`aem/assets/contenthub/1`: asset details tab panels, card/selection-bar namespaces, Host APIs, web actions). +- Use `references/aem-extensions.md` for AEM UI Extension patterns (`@adobe/uix-guest`) — Content Fragment Console/Editor, Universal Editor, Assets View, and Content Hub. - Use `references/debugging.md` for common SPA debugging scenarios (shell spinner, CORS, auth, blank screen, performance). ## Chaining From d5903d569d99dea0c5e7aabe9bbde71cf13ed06c Mon Sep 17 00:00:00 2001 From: Naradasi-Tejaswi-3011 Date: Fri, 10 Jul 2026 16:07:51 +0530 Subject: [PATCH 4/6] Refactor SKILL.md to improve formatting Removed redundant line break in SKILL.md for clarity. --- plugins/app-builder/skills/appbuilder-e2e-testing/SKILL.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/app-builder/skills/appbuilder-e2e-testing/SKILL.md b/plugins/app-builder/skills/appbuilder-e2e-testing/SKILL.md index 346b852f..7bf277a9 100644 --- a/plugins/app-builder/skills/appbuilder-e2e-testing/SKILL.md +++ b/plugins/app-builder/skills/appbuilder-e2e-testing/SKILL.md @@ -5,8 +5,7 @@ description: >- application. Covers Playwright E2E testing for ExC Shell SPAs, AEM extension UIs (Content Fragment Console, Universal Editor, Content Hub), and full-stack flows. Use when the user mentions: "E2E test", "end-to-end test", "Playwright", "browser test", "test my SPA in the browser", "test my AEM - extension", "test the - full flow", "integration test with UI", "headless browser test", "E2E in CI". + extension", "test the full flow", "integration test with UI", "headless browser test", "E2E in CI". This skill is for BROWSER-based testing only. For Jest unit tests of actions or React components, use appbuilder-testing instead. metadata: From b657e5c7d890d59c6d5e7c49111bd8f05aba3501 Mon Sep 17 00:00:00 2001 From: Naradasi Tejaswi Date: Fri, 10 Jul 2026 16:12:19 +0530 Subject: [PATCH 5/6] feat(contenthub): update format --- .../app-builder/skills/appbuilder-action-scaffolder/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/app-builder/skills/appbuilder-action-scaffolder/SKILL.md b/plugins/app-builder/skills/appbuilder-action-scaffolder/SKILL.md index ba604e76..e440c715 100644 --- a/plugins/app-builder/skills/appbuilder-action-scaffolder/SKILL.md +++ b/plugins/app-builder/skills/appbuilder-action-scaffolder/SKILL.md @@ -104,7 +104,7 @@ If there is any ambiguity — multiple patterns could fit, constraints are uncle - Use `references/runtime-reference.md` for action structure, params, response formats, SDK usage, and CLI operations. - Use `references/aem-apis.md` for AEM Content Fragment API surfaces — decision table, Delivery OpenAPI, Management OpenAPI, GraphQL persisted queries, and the deprecated Assets HTTP API — with auth patterns and action code for each. - Use `references/action-patterns.md` for common action patterns, including CRUD API, cron, multi-step processing, database CRUD, webhook intake, custom event providers, journaling consumers, large payload redirects, action sequence composition, and Asset Compute workers. -- Use `assets/database-action-template.js`, `assets/event-webhook-template.js`, `assets/event-provider-template.js`, `assets/journaling-consumer-template.js`, `assets/large-payload-template.js`, `assets/action-sequence-template.js`, `assets/asset-compute-worker-template.js` when the user request maps directly to one of the newer boilerplate patterns. +- Use `assets/database-action-template.js`, `assets/event-webhook-template.js`, `assets/event-provider-template.js`, `assets/journaling-consumer-template.js`, `assets/large-payload-template.js`, `assets/action-sequence-template.js`, and `assets/asset-compute-worker-template.js` when the user request maps directly to one of the newer boilerplate patterns. - Use `../_shared/categories/architecture-runtime.md` for Adobe service-specific guidance. ## Common Manifest Guardrail From 5ff42b3228b02a5159995f04c9f874ceebefc0f9 Mon Sep 17 00:00:00 2001 From: Naradasi Tejaswi Date: Tue, 14 Jul 2026 10:16:05 +0530 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20revert=20e2e=20and=20testing=20SKILL?= =?UTF-8?q?.md=20changes=20=E2=80=94=20no=20Content=20Hub=20patterns=20to?= =?UTF-8?q?=20back=20them?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../skills/appbuilder-e2e-testing/SKILL.md | 8 ++++---- .../app-builder/skills/appbuilder-testing/SKILL.md | 14 +++++++------- .../skills/appbuilder-ui-scaffolder/SKILL.md | 11 +++++------ 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/plugins/app-builder/skills/appbuilder-e2e-testing/SKILL.md b/plugins/app-builder/skills/appbuilder-e2e-testing/SKILL.md index 7bf277a9..abbd9f0d 100644 --- a/plugins/app-builder/skills/appbuilder-e2e-testing/SKILL.md +++ b/plugins/app-builder/skills/appbuilder-e2e-testing/SKILL.md @@ -2,10 +2,10 @@ name: appbuilder-e2e-testing description: >- Use this skill whenever the user wants browser-based end-to-end tests for an Adobe App Builder - application. Covers Playwright E2E testing for ExC Shell SPAs, AEM extension UIs (Content Fragment - Console, Universal Editor, Content Hub), and full-stack flows. Use when the user mentions: "E2E - test", "end-to-end test", "Playwright", "browser test", "test my SPA in the browser", "test my AEM - extension", "test the full flow", "integration test with UI", "headless browser test", "E2E in CI". + application. Covers Playwright E2E testing for ExC Shell SPAs, AEM extension UIs, and full-stack + flows. Use when the user mentions: "E2E test", "end-to-end test", "Playwright", "browser test", + "test my SPA in the browser", "test my AEM extension", "test the full flow", "integration test + with UI", "headless browser test", "E2E in CI". This skill is for BROWSER-based testing only. For Jest unit tests of actions or React components, use appbuilder-testing instead. metadata: diff --git a/plugins/app-builder/skills/appbuilder-testing/SKILL.md b/plugins/app-builder/skills/appbuilder-testing/SKILL.md index 9e85a8b9..6e5db2f2 100644 --- a/plugins/app-builder/skills/appbuilder-testing/SKILL.md +++ b/plugins/app-builder/skills/appbuilder-testing/SKILL.md @@ -4,13 +4,13 @@ description: >- Generate and run tests for Adobe App Builder actions and UI components. Scaffolds Jest unit tests, integration tests against deployed actions, contract tests for Adobe API interactions, and React component tests using Testing Library. Provides mock helpers for State, Files, Events SDKs, - @adobe/aio-lib-* clients, ExC Shell context (@adobe/exc-app), and UIX Guest SDK (@adobe/uix-guest) - for AEM and Content Hub extension components. Use this skill whenever the user mentions testing App Builder actions, writing - unit tests for Runtime actions, creating integration tests, mocking Adobe SDKs, setting up test - fixtures, running aio app test, or wants to verify action behavior before deployment. Also trigger - when users mention Jest configuration for App Builder, test coverage, CI test setup, React component - test, Testing Library, UI test, mock shell/extension context, debug - test failures, or fix Jest errors. + @adobe/aio-lib-* clients, ExC Shell context (@adobe/exc-app), and UIX Guest SDK (@adobe/uix-guest). + Use this skill whenever the user mentions testing App Builder actions, writing unit tests for + Runtime actions, creating integration tests, mocking Adobe SDKs, setting up test fixtures, running + aio app test, or wants to verify action behavior before deployment. Also trigger when users mention + Jest configuration for App Builder, test coverage, CI test setup, React component test, Testing + Library, UI test, Provider wrapper, test my page, test my form, test my table, test my component, + mock shell context, mock extension context, debug test failures, or fix Jest errors. metadata: category: testing license: Apache-2.0 diff --git a/plugins/app-builder/skills/appbuilder-ui-scaffolder/SKILL.md b/plugins/app-builder/skills/appbuilder-ui-scaffolder/SKILL.md index 19a6f68e..dedd0df9 100644 --- a/plugins/app-builder/skills/appbuilder-ui-scaffolder/SKILL.md +++ b/plugins/app-builder/skills/appbuilder-ui-scaffolder/SKILL.md @@ -4,14 +4,13 @@ description: >- Generate React Spectrum UI components for Adobe Experience Cloud Shell SPAs and AEM UI Extensions. Provides patterns for pages, forms, data tables, dialogs, and navigation using @adobe/react-spectrum. Guides ExC Shell integration with @adobe/exc-app including runtime.done(), IMS token passthrough, - and shell theming. Guides AEM UI Extension development with @adobe/uix-guest for Content Fragment - Console, CF Editor, Universal Editor, Assets View, and Content Hub surfaces (Content Hub via - aem/assets/contenthub/1 — asset details panels, card actions, bulk actions). + and shell theming. Guides AEM UI Extension development with @adobe/uix-guest, including Content Hub + surfaces (aem/assets/contenthub/1 — asset details panels, card actions, bulk actions). Trigger on: building App Builder UI, React Spectrum components, ExC Shell pages, forms, data tables, - dialogs, modals, navigation, theming, web-src, Spectrum, @adobe/exc-app, AEM extension, + dialogs, modals, navigation, theming, web-src, Spectrum design system, @adobe/exc-app, AEM extension, AEM UI extension, Content Fragment Console, Universal Editor extension, Content Hub panel, Content Hub - card action, Content Hub bulk action, uix-guest, @adobe/uix-guest, extension points - for AEM, customizing AEM surfaces. + card action, Content Hub bulk action, uix-guest, @adobe/uix-guest, extension points for AEM, + customizing AEM surfaces. metadata: category: frontend license: Apache-2.0