diff --git a/src/pages/config.md b/src/pages/config.md index 32841bc1..e2e3ada4 100644 --- a/src/pages/config.md +++ b/src/pages/config.md @@ -11,6 +11,7 @@ - [Universal Editor](/services/aem-universal-editor/index.md) - [Adobe Commerce Admin](https://developer.adobe.com/commerce/extensibility/admin-ui-sdk/) - [AEM Assets View](/services/aem-assets-view/index.md) + - [Content Hub](/services/contenthub/index.md) - [AEM Experience Hub](/services/aem-experience-hub/index.md) - [Extension Manager](/extension-manager/index.md) @@ -55,6 +56,15 @@ - [Step-by-step Extension Development](/services/aem-assets-view/extension-development/index.md) - [Code Generation](/services/aem-assets-view/code-generation/index.md) - [Troubleshooting](/services/aem-assets-view/debug/index.md) + - [Content Hub](/services/contenthub/index.md) + - [Extension Points](/services/contenthub/api/index.md) + - [Common Concepts](/services/contenthub/api/commons/index.md) + - [Card Actions](/services/contenthub/api/card-actions/index.md) + - [Asset Details Tab Panels](/services/contenthub/api/asset-details/index.md) + - [Selection Bar Actions](/services/contenthub/api/selection-bar/index.md) + - [Step-by-step Extension Development](/services/contenthub/extension-development/index.md) + - [Code Generation](/services/contenthub/code-generation/index.md) + - [Troubleshooting](/services/contenthub/debug/index.md) - [Extension Manager](/extension-manager/index.md) - [Feature Highlights](/extension-manager/feature-highlights/index.md) - [Extensions Developed By Adobe](/extension-manager/extension-developed-by-adobe/index.md) diff --git a/src/pages/services/contenthub/api/asset-details/asset-details-tab-panel.png b/src/pages/services/contenthub/api/asset-details/asset-details-tab-panel.png new file mode 100644 index 00000000..e97da4d0 Binary files /dev/null and b/src/pages/services/contenthub/api/asset-details/asset-details-tab-panel.png differ diff --git a/src/pages/services/contenthub/api/asset-details/index.md b/src/pages/services/contenthub/api/asset-details/index.md new file mode 100644 index 00000000..b722e0bb --- /dev/null +++ b/src/pages/services/contenthub/api/asset-details/index.md @@ -0,0 +1,234 @@ +--- +title: Asset Details Tab Panels - Content Hub Extensibility +description: Add custom tab panels to the Asset Details dialog in Content Hub. +contributors: + - https://github.com/AdobeDocs/uix +--- + +# Asset Details Tab Panels + +Content Hub lets extensions add custom tab panels to the **Asset Details dialog** — the dialog that opens when a user clicks on an asset. + +![Asset details dialog with custom tab panel](asset-details-tab-panel.png) + +Custom tabs appear alongside the built-in tabs in the Asset Details dialog. Content Hub manages tab switching and lifecycle; the extension only provides tab metadata and the URL that renders the tab body. + +Extensions use the `aem/assets/contenthub/1` extension point and implement the `assetDetails` namespace inside a single `register()` call. + +## Host API Reference + +In addition to the [Common APIs](../commons/index.md), the `assetDetails` namespace exposes one additional method on `guestConnection.host`: + +### `assetDetails.getCurrentAsset()` + +**Description:** Returns the identifier of the asset currently open in the Asset Details dialog. + +**Returns** (`string`): Asset URN (e.g. `urn:aaid:aem:...`). + +**Important:** `getCurrentAsset()` returns a **plain string**, not an object. If your downstream code expects an object, wrap it as `{ id: assetId }`; otherwise use the string directly: + +```js +const assetId = await connection.host.assetDetails.getCurrentAsset(); +// use assetId directly, or wrap: const asset = { id: assetId }; +``` + +## Extension API Reference + +### `assetDetails` namespace + +#### `assetDetails.getTabPanels()` + +**Description:** Returns the list of custom tab panels to add to the Asset Details dialog. + +**Returns** (`array`): Array of tab panel descriptor objects: +- `id` (`string`): Panel ID, unique within the extension. +- `tooltip` (`string`): Tooltip shown on the tab icon. +- `title` (`string`): Tab label text. +- `icon` (`string`): [React Spectrum workflow icon](https://react-spectrum.adobe.com/react-spectrum/workflow-icons.html#available-icons) name. +- `contentUrl` (`string`): Hash-relative URL to the panel content (e.g. `/#asset-details-extension-tab`). + +## Example + +This example adds an **Extension Template** panel that displays the current asset's URN and a button that shows a toast notification. + +### `ExtensionRegistration.js` — registration + +```js +import React from 'react'; +import { Text } from '@adobe/react-spectrum'; +import { register } from '@adobe/uix-guest'; +import { extensionId } from './Constants'; + +function ExtensionRegistration() { + const init = async () => { + let guestConnection = await register({ + id: extensionId, + methods: { + assetDetails: { + getTabPanels() { + return [ + { + id: 'extension-template', + tooltip: 'Extension Template', + icon: 'Extension', + title: 'Extension Template', + contentUrl: '/#asset-details-extension-tab', + }, + ]; + }, + }, + }, + }); + }; + + init().catch(console.error); + return IFrame for integration with Host (Content Hub)...; +} + +export default ExtensionRegistration; +``` + +### `App.js` — routing + +```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 PanelAssetDetailsExtensionTab from './PanelAssetDetailsExtensionTab'; + +function App() { + return ( + + + + } /> + } /> + } /> + + + + ); + + function onError(e, componentStack) {} + function fallbackComponent({ componentStack, error }) { + return ( + +

Extension rendering error

+
{componentStack + '\n' + error.message}
+
+ ); + } +} + +export default App; +``` + +### `PanelAssetDetailsExtensionTab.js` — panel content + +The panel component calls `attach()` to connect to Content Hub and retrieves the current asset. Note that `getCurrentAsset()` is **asynchronous** and returns a plain string — normalize it to `{ id }`. + +```js +import React, { useState, useEffect } from 'react'; +import { attach } from '@adobe/uix-guest'; +import { + Provider, + defaultTheme, + View, + Heading, + Text, + Button, + Divider, + ProgressCircle, +} from '@adobe/react-spectrum'; +import { extensionId } from './Constants'; + +export default function PanelAssetDetailsExtensionTab() { + const [guestConnection, setGuestConnection] = useState(null); + const [asset, setAsset] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + (async () => { + try { + const connection = await attach({ id: extensionId }); + setGuestConnection(connection); + + // getCurrentAsset() returns the asset id as a plain string (e.g. "urn:aaid:aem:..."). + const assetId = await connection.host.assetDetails.getCurrentAsset(); + setAsset({ id: assetId }); + } finally { + setLoading(false); + } + })(); + }, []); + + if (loading) { + return ( + + + + + + ); + } + + return ( + + + Extension Template + + {asset && ( + + Asset ID: + + + {asset.id} + + + + )} + + + + ); +} +``` + +## Calling a backend web action from the panel + +To make AEM API calls from the panel, retrieve auth info from the host and call your Adobe I/O Runtime action: + +```js +// Add this import at the top of PanelAssetDetailsExtensionTab.js alongside the other imports: +import actions from '../config.json'; + +// Inside the useEffect, after attach(): +const { accessToken, imsOrg } = await connection.host.auth.getIMSInfo(); +const apiKey = await connection.host.auth.getApiKey(); +const aemHost = await connection.host.discovery.getAemHost(); + +const response = await fetch(actions['aem-assets-contenthub-1/generic'], { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ assetId: asset.id, aemHost, apiKey, imsOrg }), +}); +const data = await response.json(); +``` + +`config.json` is generated by the CLI template and contains the deployed web action URLs keyed by action name. + +## Additional resources + +- [Common Concepts](../commons/index.md) +- [Card Actions](../card-actions/index.md) +- [Selection Bar Actions](../selection-bar/index.md) +- [Step-by-step Extension Development](../../extension-development/index.md) +- [Troubleshooting](../../debug/index.md) diff --git a/src/pages/services/contenthub/api/card-actions/asset-card-actions.png b/src/pages/services/contenthub/api/card-actions/asset-card-actions.png new file mode 100644 index 00000000..2f342285 Binary files /dev/null and b/src/pages/services/contenthub/api/card-actions/asset-card-actions.png differ diff --git a/src/pages/services/contenthub/api/card-actions/collection-card-actions.png b/src/pages/services/contenthub/api/card-actions/collection-card-actions.png new file mode 100644 index 00000000..fc3fa875 Binary files /dev/null and b/src/pages/services/contenthub/api/card-actions/collection-card-actions.png differ diff --git a/src/pages/services/contenthub/api/card-actions/index.md b/src/pages/services/contenthub/api/card-actions/index.md new file mode 100644 index 00000000..5972695f --- /dev/null +++ b/src/pages/services/contenthub/api/card-actions/index.md @@ -0,0 +1,231 @@ +--- +title: Card Actions - Content Hub Extensibility +description: Add custom action buttons to asset cards and collection tiles in Content Hub. +contributors: + - https://github.com/AdobeDocs/uix +--- + +# Card Actions + +Content Hub lets extensions add custom action buttons to the **asset card menu** and the **collection tile menu** using a single shared `card` namespace. + +![Asset card with custom actions](asset-card-actions.png) + +![Collection card with custom actions](collection-card-actions.png) + +Card actions appear wherever asset cards or collection tiles are rendered. Use the `context` value passed to each method to differentiate the surface: + +| `context` value | Surface | +|---|---| +| `'assets'` | Main Assets browse grid | +| `'collection'` | Asset card inside an open collection | +| `'collections'` | Collection tile on the Collections grid | +| `'share'` | Asset card in a link-share view | + +Extensions use the `aem/assets/contenthub/1` extension point and implement the `card` namespace inside a single `register()` call. + +## Extension API Reference + +### `card` namespace + +#### `card.getActionButtons(actionContext)` + +**Description:** Returns the list of custom buttons to add to the card menu for the given context. + +**Parameters:** +- `actionContext` (`object`): + - `context` (`string`): The surface where the card is rendered — `'assets'`, `'collection'`, `'collections'`, or `'share'`. + +**Returns** (`array`): An array of button descriptor objects. Each object contains: +- `id` (`string`): Unique identifier for the button within the extension. +- `label` (`string`): Button label shown in the menu. +- `icon` (`string`): [React Spectrum workflow icon](https://react-spectrum.adobe.com/react-spectrum/workflow-icons.html#available-icons) name. + +Return an empty array if no buttons should be shown for the given context. + +#### `card.onActionClick(resourceType, buttonId, resourceId, actionContext)` + +**Description:** Called by Content Hub when the user clicks a custom card button. + +**Parameters:** +- `resourceType` (`string`): `'asset'` for asset cards; `'collection'` for collection tiles. +- `buttonId` (`string`): The `id` of the button that was clicked. +- `resourceId` (`string`): The URN or ID of the asset or collection the card represents. +- `actionContext` (`object`): Same context object passed to `getActionButtons`. + +## Example + +This example adds a custom button to asset cards (main Assets grid, inside collections) and to collection tiles (Collections grid). A single `card` registration block and a single modal component serve both surfaces — `resourceType` tells the modal which kind of resource was clicked. + +### `App.js` — routing + +```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 CardActionModal from './CardActionModal'; + +function App() { + return ( + + + + } /> + } /> + } /> + + + + ); + + function onError(e, componentStack) {} + function fallbackComponent({ componentStack, error }) { + return ( + +

Extension rendering error

+
{componentStack + '\n' + error.message}
+
+ ); + } +} + +export default App; +``` + +### `ExtensionRegistration.js` — registration + +```js +import React from 'react'; +import { Text } from '@adobe/react-spectrum'; +import { register } from '@adobe/uix-guest'; +import { extensionId } from './Constants'; + +function ExtensionRegistration() { + const init = async () => { + let guestConnection = await register({ + id: extensionId, + methods: { + card: { + getActionButtons(actionContext) { + const { context } = actionContext || {}; + // Show the button on asset cards and on collection tiles + if (context !== 'assets' && context !== 'collection' && context !== 'collections') { + return []; + } + return [ + { + id: 'customId', + label: 'Custom label', + icon: 'Form', + }, + ]; + }, + async onActionClick(resourceType, buttonId, resourceId, actionContext) { + if (buttonId === 'customId') { + await guestConnection.host.modal.openDialog({ + title: 'Custom Dialog', + contentUrl: `/#card-action-modal?resourceId=${encodeURIComponent(resourceId)}&resourceType=${encodeURIComponent(resourceType)}`, + type: 'modal', + size: 'M', + }); + } + }, + }, + }, + }); + }; + + init().catch(console.error); + return IFrame for integration with Host (Content Hub)...; +} + +export default ExtensionRegistration; +``` + +### `CardActionModal.js` — dialog content + +```js +import React, { useState, useEffect } from 'react'; +import { attach } from '@adobe/uix-guest'; +import { + Provider, + defaultTheme, + View, + Heading, + Text, + Button, + ButtonGroup, + Divider, + 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 () => { + const connection = await attach({ id: extensionId }); + setGuestConnection(connection); + + // Read data passed via URL query parameters + const params = new URLSearchParams(window.location.hash.split('?')[1] || ''); + setPayload({ + resourceId: params.get('resourceId'), + resourceType: params.get('resourceType'), + }); + })(); + }, []); + + if (!payload) { + return ( + + + + + + ); + } + + const handleExport = async () => { + // Add your export logic here + await guestConnection?.host.toast.display({ + variant: 'positive', + message: `Custom action on ${payload.resourceType}: ${payload.resourceId}`, + }); + guestConnection?.host.modal.closeDialog(); + }; + + return ( + + + Custom Dialog + + + Type: {payload.resourceType} + + + {payload.resourceId} + + + + + + + + + + ); +} +``` + +## Additional resources + +- [Common Concepts](../commons/index.md) +- [Asset Details Tab Panels](../asset-details/index.md) +- [Selection Bar Actions](../selection-bar/index.md) +- [Step-by-step Extension Development](../../extension-development/index.md) +- [Troubleshooting](../../debug/index.md) diff --git a/src/pages/services/contenthub/api/commons/index.md b/src/pages/services/contenthub/api/commons/index.md new file mode 100644 index 00000000..33e69126 --- /dev/null +++ b/src/pages/services/contenthub/api/commons/index.md @@ -0,0 +1,291 @@ +--- +title: Common Concepts - Content Hub Extensibility +description: Learn about extension registration and common Host APIs available to every Content Hub extension. +contributors: + - https://github.com/AdobeDocs/uix +--- + +# Common Concepts in Creating Extensions + +This page explains the fundamentals shared by every Content Hub extension: the extension point, how to register an extension, and the Host APIs that are available to all surfaces. + +## Extension Point + +All Content Hub UI extensions use the `aem/assets/contenthub/1` extension point. + +Declare it in your `app.config.yaml`: + +```yaml +extensions: + aem/assets/contenthub/1: + $include: src/aem-assets-contenthub-1/ext.config.yaml +``` + +A typical `ext.config.yaml`: + +```yaml +operations: + view: + - type: web + impl: index.html +actions: actions +web: web-src +``` + +## Extension Registration + +An extension announces its capabilities to Content Hub by calling `register()` from `@adobe/uix-guest`. This function is asynchronous and returns a connection object used to interact with the host. + +All three namespaces (`assetDetails`, `card`, `selectionBar`) are declared in a single `register()` call. + +Use `let` for `guestConnection` so the `card` and `selectionBar` click handlers can close over it after `register()` resolves. + +```js +import { register } from '@adobe/uix-guest'; + +const init = async () => { + let guestConnection = await register({ + id: extensionId, + methods: { + assetDetails: { + getTabPanels() { /* ... */ }, + }, + card: { + getActionButtons(actionContext) { /* ... */ }, + async onActionClick(resourceType, buttonId, resourceId, actionContext) { /* ... */ }, + }, + selectionBar: { + getActionButtons(actionContext) { /* ... */ }, + async onActionClick(buttonId, assetIds) { /* ... */ }, + }, + }, + }); +}; +init().catch(console.error); +``` + +`register()` must be called from the extension's initialization page after it loads. + +The object passed to `register()` must include: +- `id` — a unique string identifier for the extension (used for debugging and for `attach()` calls from panel/modal components). +- `methods` — namespaces that correspond to Content Hub extension surfaces. + +## Restricting extensions to specific repositories + +For production extensions you should restrict registration to specific Content Hub repositories. Content Hub injects the current repository ID as a `repo` query parameter on the extension URL. + +```js +// Populate before deploying to Production. Empty = allow any repo (safe for dev). +const allowedRepos = [ + 'delivery-p12345-e167890.adobeaemcloud.com', +]; + +function getRepo() { + const search = new URLSearchParams(window.location.search); + return search.get('repo'); +} + +function shouldSkipRegistration(repo) { + return allowedRepos.length > 0 && !allowedRepos.includes(repo); +} + +function ExtensionRegistration() { + const repo = getRepo(); + if (shouldSkipRegistration(repo)) { + return Skipped registration: repo not in allowedRepos; + } + // proceed with init() and register() ... +} +``` + +## Building Extension UI + +Extension panels and modal dialogs are rendered inside iframes. A panel or dialog component calls `attach()` to get the connection object: + +```js +import { attach } from '@adobe/uix-guest'; + +const guestConnection = await attach({ id: extensionId }); +``` + +After attaching, the component can call Host APIs on `guestConnection.host`. + +## Passing data to modal dialogs + +The `modal.openDialog()` `contentUrl` is a hash-relative URL rendered inside an iframe. Pass data to a dialog via **URL query parameters** embedded in `contentUrl`: + +```js +// In ExtensionRegistration.js — opening the dialog: +await guestConnection.host.modal.openDialog({ + title: 'Custom Dialog', + contentUrl: `/#my-dialog?assetId=${encodeURIComponent(assetId)}`, + type: 'modal', + size: 'M', +}); + +// In the dialog component — reading the data: +const params = new URLSearchParams(window.location.hash.split('?')[1] || ''); +const assetId = params.get('assetId'); +``` + +## Common APIs exposed by Content Hub to all extensions + +The APIs below are available to every Content Hub extension regardless of which surface it extends. All API calls are asynchronous and return a `Promise`. + +--- + +### Authentication API (`auth`) + +#### `auth.getIMSInfo()` + +**Description:** Returns IMS organization information and the current access token. + +**Returns:** +- `imsOrg` (`string`): IMS organization identifier. +- `imsOrgName` (`string`): Human-readable IMS organization name. +- `accessToken` (`string`): Current IMS access token. + +**Example:** + +```js +const { imsOrg, imsOrgName, accessToken } = await guestConnection.host.auth.getIMSInfo(); +``` + +#### `auth.getApiKey()` + +**Description:** Returns the API key used by Content Hub. + +**Returns** (`string`): API key. + +**Example:** + +```js +const apiKey = await guestConnection.host.auth.getApiKey(); +``` + +--- + +### Discovery API (`discovery`) + +#### `discovery.getAemHost()` + +**Description:** Returns the AEM host URL of the delivery repository connected to this Content Hub instance. + +**Returns** (`string`): Full AEM host URL including protocol and trailing slash (e.g. `https://delivery-p12345-e123456.adobeaemcloud.com/`). + +**Example:** + +```js +const aemHost = await guestConnection.host.discovery.getAemHost(); +// e.g. "https://delivery-p12345-e123456.adobeaemcloud.com/" +``` + +--- + +### Toast API (`toast`) + +#### `toast.display({ variant, message })` + +**Description:** Shows a toast notification in Content Hub. + +**Parameters:** +- `variant` (`string`, optional): `'neutral'`, `'positive'`, `'negative'`, or `'info'`. Defaults to `'info'`. +- `message` (`string`, required): Text to display. + +**Example:** + +```js +guestConnection.host.toast.display({ variant: 'positive', message: 'Asset exported successfully' }); +``` + +--- + +### Internationalization API (`i18n`) + +#### `i18n.getLocalizationInfo()` + +**Description:** Returns the locale currently active in Content Hub. + +**Returns:** +- `locale` (`string`): BCP 47 locale tag (e.g. `en-US`, `fr-FR`). + +**Example:** + +```js +const { locale } = await guestConnection.host.i18n.getLocalizationInfo(); +``` + +--- + +### Modal API (`modal`) + +#### `modal.openDialog(options)` + +**Description:** Opens a dialog. The dialog content is loaded from `contentUrl`. + +**Parameters (`options` object):** +- `title` (`string`, required): Dialog heading. +- `contentUrl` (`string`, required): Hash-relative URL to the dialog content page within the extension (e.g. `/#my-dialog`). To pass data, either embed it as URL query parameters (e.g. `/#my-dialog?assetId=...`) or use the `payload` field below. +- `type` (`string`, optional): `'modal'` (default) or `'fullscreen'`. +- `size` (`string`, optional): `'S'`, `'M'` (default), or `'L'`. Ignored when `type` is `'fullscreen'`. +- `payload` (`any`, optional): Arbitrary data object the dialog component can retrieve with `modal.getPayload()`. + +**Recommended pattern — pass data via URL query parameters** (simpler, synchronous, works without async `getPayload()` round-trip): + +```js +await guestConnection.host.modal.openDialog({ + title: 'Custom Dialog', + contentUrl: `/#export-dialog?assetId=${encodeURIComponent(assetId)}`, + type: 'modal', + size: 'M', +}); + +// Inside the dialog component — read from the URL: +const params = new URLSearchParams(window.location.hash.split('?')[1] || ''); +const assetId = params.get('assetId'); +``` + +**Alternative — pass data via `payload`:** + +```js +// In ExtensionRegistration.js — opening the dialog with a payload: +await guestConnection.host.modal.openDialog({ + title: 'Custom Dialog', + contentUrl: '/#export-dialog', + type: 'modal', + size: 'M', + payload: { assetId }, +}); +``` + +```js +// Inside the dialog component — attach() first, then read the payload: +const connection = await attach({ id: extensionId }); +const { assetId } = await connection.host.modal.getPayload(); +``` + +#### `modal.closeDialog()` + +**Description:** Closes the currently active dialog. + +**Example:** + +```js +guestConnection.host.modal.closeDialog(); +// or using optional chaining from a dialog component: +guestConnection?.host.modal.closeDialog(); +``` + +#### `modal.getPayload()` + +**Description:** Returns the payload object passed when `modal.openDialog()` was called. Call this from the dialog component after `attach()`. + +**Returns:** The payload value passed in `openDialog({ payload })`, or `undefined` if no payload was provided. + +**Example:** + +```js +// In the dialog component: +const connection = await attach({ id: extensionId }); +const payload = await connection.host.modal.getPayload(); +``` diff --git a/src/pages/services/contenthub/api/index.md b/src/pages/services/contenthub/api/index.md new file mode 100644 index 00000000..d7642d7d --- /dev/null +++ b/src/pages/services/contenthub/api/index.md @@ -0,0 +1,36 @@ +--- +title: Content Hub Extensibility - Extension Points +description: Browse all available extension surfaces in Content Hub. +contributors: + - https://github.com/AdobeDocs/uix +--- + +# Content Hub Extension Points + +This section covers the available extension surfaces, extension registration, and common methods available to every Content Hub extension. + +All Content Hub extensions use the **`aem/assets/contenthub/1`** extension point. + + + +[Common Concepts in Creating Extensions](commons/index.md) + +Learn about extension registration, the Host API, and APIs available to every extension (auth, discovery, toast, i18n, modal) + + + +[Card Actions](card-actions/index.md) + +Add custom action buttons to asset cards and collection tiles in the Assets grid, inside Collections, and in-app Link Share + + + +[Asset Details Tab Panels](asset-details/index.md) + +Add custom tab panels to the Asset Details dialog + + + +[Selection Bar](selection-bar/index.md) + +Add custom bulk-action buttons to the blue Selection Bar that appears when one or more assets are selected diff --git a/src/pages/services/contenthub/api/selection-bar/index.md b/src/pages/services/contenthub/api/selection-bar/index.md new file mode 100644 index 00000000..9d0236c6 --- /dev/null +++ b/src/pages/services/contenthub/api/selection-bar/index.md @@ -0,0 +1,229 @@ +--- +title: Selection Bar Actions - Content Hub Extensibility +description: Add custom bulk-action buttons to the Content Hub Selection Bar. +contributors: + - https://github.com/AdobeDocs/uix +--- + +# Selection Bar Actions + +Content Hub lets extensions add custom buttons to the **Selection Bar** — the bar that appears at the top of the screen when one or more assets are selected. + +![Selection Bar with custom actions](selection-bar.png) + +Selection Bar actions allow bulk operations: the extension receives the IDs of all selected assets when the button is clicked. + +Extensions use the `aem/assets/contenthub/1` extension point and implement the `selectionBar` namespace inside a single `register()` call. + +## Extension API Reference + +### `selectionBar` namespace + +#### `selectionBar.getActionButtons(actionContext)` + +**Description:** Returns the list of custom buttons to add to the Selection Bar. + +**Parameters:** +- `actionContext` (`object`): + - `context` (`string`): The surface where assets are selected — `'assets'`, `'collection'`, `'collections'`, or `'share'`. + - `resourceSelection` (`object`): + - `resources` (`array`): Array of selected resource objects, each with an `id` property. + +**Returns** (`array`): An array of button descriptor objects: +- `id` (`string`): Unique identifier for the button within the extension. +- `label` (`string`): Button label shown in the Selection Bar. +- `icon` (`string`): [React Spectrum workflow icon](https://react-spectrum.adobe.com/react-spectrum/workflow-icons.html#available-icons) name. + +Return an empty array if no buttons should be shown. + +#### `selectionBar.onActionClick(buttonId, assetIds)` + +**Description:** Called by Content Hub when the user clicks a custom Selection Bar button. + +**Parameters:** +- `buttonId` (`string`): The `id` of the button that was clicked. +- `assetIds` (`array`): Array of asset ID strings for all currently selected assets. + +## Example + +This example adds a custom button to the Selection Bar that opens a dialog showing all selected asset IDs. + +### `App.js` — routing + +```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 SelectionBarModal from './SelectionBarModal'; + +function App() { + return ( + + + + } /> + } /> + } /> + + + + ); + + function onError(e, componentStack) {} + function fallbackComponent({ componentStack, error }) { + return ( + +

Extension rendering error

+
{componentStack + '\n' + error.message}
+
+ ); + } +} + +export default App; +``` + +### `ExtensionRegistration.js` — registration + +```js +import React from 'react'; +import { Text } from '@adobe/react-spectrum'; +import { register } from '@adobe/uix-guest'; +import { extensionId } from './Constants'; + +function ExtensionRegistration() { + const init = async () => { + let guestConnection = await register({ + id: extensionId, + methods: { + selectionBar: { + getActionButtons(actionContext) { + const { context } = actionContext || {}; + // Show button only on the main assets grid + if (context !== 'assets') { + return []; + } + return [ + { + id: 'customId', + label: 'Custom label', + icon: 'Form', + }, + ]; + }, + async onActionClick(buttonId, assetIds) { + if (buttonId === 'customId') { + await guestConnection.host.modal.openDialog({ + title: `Custom Dialog (${assetIds.length} asset${assetIds.length !== 1 ? 's' : ''} selected)`, + contentUrl: `/#selection-bar-modal?assetIds=${encodeURIComponent(JSON.stringify(assetIds))}`, + type: 'modal', + size: 'M', + }); + } + }, + }, + }, + }); + }; + + init().catch(console.error); + return IFrame for integration with Host (Content Hub)...; +} + +export default ExtensionRegistration; +``` + +### `SelectionBarModal.js` — dialog content + +The assetIds are passed as a JSON-encoded URL parameter and decoded inside the dialog component. + +```js +import React, { useState, useEffect } from 'react'; +import { attach } from '@adobe/uix-guest'; +import { + Provider, + defaultTheme, + View, + Heading, + Text, + Button, + ButtonGroup, + Divider, + ListView, + Item, + ProgressCircle, +} from '@adobe/react-spectrum'; +import { extensionId } from './Constants'; + +export default function SelectionBarModal() { + const [guestConnection, setGuestConnection] = useState(null); + const [payload, setPayload] = useState(null); + + useEffect(() => { + (async () => { + const connection = await attach({ id: extensionId }); + setGuestConnection(connection); + + // assetIds are passed as a JSON-encoded URL parameter + const params = new URLSearchParams(window.location.hash.split('?')[1] || ''); + const raw = params.get('assetIds'); + setPayload({ + assetIds: raw ? JSON.parse(raw) : [], + }); + })(); + }, []); + + if (!payload) { + return ( + + + + + + ); + } + + const handleExport = async () => { + // Add your bulk export logic here + await guestConnection?.host.toast.display({ + variant: 'positive', + message: `Custom action on ${payload.assetIds.length} asset(s)`, + }); + guestConnection?.host.modal.closeDialog(); + }; + + return ( + + + Custom Dialog — {payload.assetIds.length} asset(s) selected + + ({ id, name: id }))} + height="size-2400" + aria-label="Selected assets" + > + {(item) => ( + + {item.name} + + )} + + + + + + + + ); +} +``` + +## Additional resources + +- [Common Concepts](../commons/index.md) +- [Card Actions](../card-actions/index.md) +- [Asset Details Tab Panels](../asset-details/index.md) +- [Step-by-step Extension Development](../../extension-development/index.md) +- [Troubleshooting](../../debug/index.md) diff --git a/src/pages/services/contenthub/api/selection-bar/selection-bar.png b/src/pages/services/contenthub/api/selection-bar/selection-bar.png new file mode 100644 index 00000000..ba6d2987 Binary files /dev/null and b/src/pages/services/contenthub/api/selection-bar/selection-bar.png differ diff --git a/src/pages/services/contenthub/code-generation/index.md b/src/pages/services/contenthub/code-generation/index.md new file mode 100644 index 00000000..f676961e --- /dev/null +++ b/src/pages/services/contenthub/code-generation/index.md @@ -0,0 +1,153 @@ +--- +title: Sample Extension - Content Hub Extensibility +description: Get started quickly by cloning the Content Hub sample extension from adobe/aem-uix-examples. +contributors: + - https://github.com/AdobeDocs/uix +--- + +# Content Hub Sample Extension + +The quickest way to start building a Content Hub extension is to clone the official sample from [adobe/aem-uix-examples](https://github.com/adobe/aem-uix-examples). The sample is a working App Builder project that demonstrates all three extension surfaces — Asset Details tab panels, asset card actions, and Selection Bar bulk actions — in a single extension. + +Clone it, run it locally, then delete the parts you don't need and replace the rest with your business logic. + +## Prerequisites + +- Node.js 18 or higher +- [Adobe I/O CLI](https://developer.adobe.com/runtime/docs/guides/tools/cli_install/): `npm install -g @adobe/aio-cli` +- An Adobe Developer Console account with an App Builder project (I/O Runtime must be enabled on the project) +- Access to a Content Hub environment with the extensibility feature flag enabled + +## Clone the sample + +```shell +git clone https://github.com/adobe/aem-uix-examples.git +cd aem-uix-examples/aem-contenthub-sample +``` + +## Project structure + +```text +aem-contenthub-sample/ +├── app.config.yaml +├── extension-manifest.json +├── hooks/ +│ └── post-deploy.js +├── package.json +└── src/ + └── aem-assets-contenthub-1/ + ├── actions/ + │ └── generic/ + │ └── index.js ← server-side web action (optional) + ├── ext.config.yaml + └── web-src/ + └── src/ + └── components/ + ├── App.js ← routing + ├── Constants.js ← extension ID + ├── ExtensionRegistration.js ← registers all namespaces + ├── PanelAssetDetailsExtensionTab.js ← assetDetails tab panel + ├── CardActionModal.js ← card action modal + └── SelectionBarModal.js ← selection bar modal +``` + +```yaml +# app.config.yaml — declares the extension point +extensions: + aem/assets/contenthub/1: + $include: src/aem-assets-contenthub-1/ext.config.yaml +``` + +`ExtensionRegistration.js` is the entry point — it registers all three namespaces with Content Hub. `App.js` maps URL routes to each panel or modal component. + +## Setup and run locally + +```shell +# 1. Install dependencies +npm install + +# 2. Log in to Adobe I/O +aio login + +# 3. Select your org, project, and workspace +aio console org select +aio console project select +aio console workspace select + +# 4. Link the app to the selected project and populate .env +aio app use -g + +# 5. Start the local dev server (https://localhost:9080) +aio app run +``` + +## Accept the self-signed certificate + +The first time you run the dev server, open `https://localhost:9080` in your browser and accept the certificate (click **Advanced → Proceed to localhost**, or type `thisisunsafe` on the warning page). + +You only need to do this once per browser session. + +## Load the extension in Content Hub + +Open Content Hub with `devMode=true` and `ext=` pointing to your local server: + +```text +https://experience.adobe.com/?devMode=true&ext=https://localhost:9080#/assets/contenthub/ +``` + +## Verify each surface + +**Asset Details tab panel (`assetDetails`)** +1. Click any asset to open the Asset Details dialog +2. Look for the **"Asset Details Tab"** in the side rail +3. Click it — the panel loads and displays the asset's URN + +**Asset card action (`card`)** +1. Hover over any asset card — a **"Card Action"** button appears +2. Click it — a modal opens showing the resource type and ID + +**Collection tile action (`card`)** +1. Navigate to the Collections grid +2. Hover over a collection tile — a **"Collection Action"** button appears +3. Click it — a modal opens showing `resourceType: collection` + +**Selection Bar bulk action (`selectionBar`)** +1. Select one or more assets +2. The Selection Bar appears — click **"Bulk Action"** +3. A modal opens listing all selected asset URNs + +## Customize the sample + +Open `ExtensionRegistration.js` and edit the three namespace blocks to implement your logic. Remove any namespace block you don't need, and delete the matching route from `App.js` and the matching component file. + +For a detailed walkthrough of each surface and its API, see the [Extension Development guide](../extension-development/index.md) and the [API reference](../api/index.md). + +## Restrict to specific repositories + +`ExtensionRegistration.js` has an `allowedRepos` array that is empty by default — the extension registers for any Content Hub repository, which is correct for local development. Before deploying to Production, add your delivery repository IDs: + +```js +const allowedRepos = [ + 'delivery-p12345-e167890.adobeaemcloud.com', +]; +``` + +## Deploy + +```shell +# Deploy to Stage +aio app use -w Stage +aio app deploy + +# Deploy to Production +aio app use -w Production +aio app deploy +``` + +After deploying to Production, approve the extension in [Extension Manager](https://experience.adobe.com/aem/extension-manager) to make it visible to all users without the `ext=` parameter. + +## Additional resources + +- [Step-by-step Extension Development](../extension-development/index.md) +- [API Reference](../api/index.md) +- [Troubleshooting](../debug/index.md) diff --git a/src/pages/services/contenthub/debug/cert-1.png b/src/pages/services/contenthub/debug/cert-1.png new file mode 100644 index 00000000..efcd5307 Binary files /dev/null and b/src/pages/services/contenthub/debug/cert-1.png differ diff --git a/src/pages/services/contenthub/debug/cert-2.png b/src/pages/services/contenthub/debug/cert-2.png new file mode 100644 index 00000000..7fe58e3a Binary files /dev/null and b/src/pages/services/contenthub/debug/cert-2.png differ diff --git a/src/pages/services/contenthub/debug/index.md b/src/pages/services/contenthub/debug/index.md new file mode 100644 index 00000000..6b7a43a6 --- /dev/null +++ b/src/pages/services/contenthub/debug/index.md @@ -0,0 +1,143 @@ +--- +title: Troubleshooting - Content Hub Extensibility +description: Connect a locally running extension to Content Hub and resolve common issues. +contributors: + - https://github.com/AdobeDocs/uix +--- + +# Troubleshooting - Content Hub Extensibility + +Fast feedback is essential for development. You can connect a locally running Content Hub extension to the production Content Hub environment to see your changes immediately without deploying. + +## Running in a local environment + +There are two ways to run an extension locally: + +### Option 1: Complete local isolation + +Both the extension UI and serverless actions run on your machine. + +```shell +➜ my-contenthub-extension % aio app dev +``` + +```shell +To view your local application: + -> https://localhost:9080 +To view your deployed application in the Experience Cloud shell: + -> https://experience.adobe.com/?devMode=true#/custom-apps/?localDevUrl=https://localhost:9080 +Your actions: +web actions: + -> https://localhost:9080/api/v1/web/aem-assets-contenthub-1/my-action +press CTRL+C to terminate the dev environment +``` + +### Option 2: Local UI, cloud actions + +The extension UI runs locally; serverless actions are deployed to Adobe I/O Runtime. + +```shell +➜ my-contenthub-extension % aio app run +``` + +```shell +For a developer preview of your UI extension in the Content Hub environment, follow the URL: + -> https://experience.adobe.com/aem/extension-manager/preview/ + +To view your local application: + -> https://localhost:9080 +To view your deployed application in the Experience Cloud shell: + -> https://experience.adobe.com/?devMode=true#/custom-apps/?localDevUrl=https://localhost:9080 +press CTRL+C to terminate dev environment +``` + +### Extension endpoint + +The local extension is served at the URL shown next to `To view your local application` — typically `https://localhost:9080`. You will use this URL to load the extension in Content Hub. + +## Accept the Certificate + +The first time you run the extension locally, you will see: + +```shell +success: generated certificate +A self signed development certificate has been generated, you will need to accept it in your browser in order to use it. +Waiting for the certificate to be accepted.... +``` + +1. Navigate to `https://localhost:9080` in Google Chrome. + +![Certificate warning screen](cert-1.png) + +2. Click **Advanced**, then click **Proceed to localhost (unsafe)**. + +![Proceed to localhost](cert-2.png) + +In Chrome you can also type `thisisunsafe` on the warning page to bypass it. Refer to your browser's documentation for other browsers. + + +## Load UI Extension + +Once the extension is running locally, embed it in Content Hub: + +1. Navigate to Content Hub: `https://experience.adobe.com/#/assets/contenthub/` +2. Append the following query parameters to the URL: + - `devMode=true` — tells Adobe Experience Shell to allow content from localhost. + - `ext=` — the full URL of your local extension. +3. Press Enter to reload Content Hub with the extension loaded. + +**Example URL:** + +```text +https://experience.adobe.com/?devMode=true&ext=https://localhost:9080#/assets/contenthub/ +``` + +You can specify multiple `ext=` parameters to load more than one extension simultaneously: + +```text +https://experience.adobe.com/?devMode=true&ext=https://localhost:9080&ext=https://localhost:9081#/assets/contenthub/ +``` + +### `ext=` query parameter syntax + +The full syntax of the `ext=` parameter is: + +```text +ext.=[,] +``` + +For Content Hub, the extension point ID is `aem/assets/contenthub/1`. URL-encoded: + +```text +ext.aem%2fassets%2fcontenthub%2f1=https://localhost:9080 +``` + +Using the shorthand `ext=` (without specifying the extension point) applies the extension to all available extension points: + +```text +ext=https://localhost:9080 +``` + +## Common issues + +### The extension panel or action does not appear + +- Verify you accepted the self-signed certificate at `https://localhost:9080`. +- Check that `devMode=true` is in the URL. +- Open the browser DevTools console and look for errors from the UIX SDK (messages prefixed with `[uix]`). +- Confirm your `register()` call specifies the correct namespace and method names (e.g. `assetDetails.getTabPanels`). + +### Toast or modal does not appear + +- Ensure the component called `attach()` to connect to Content Hub. +- All `host.*` API calls are asynchronous; make sure you are awaiting the promises where required. + +### `register()` is never called + +- Check your `App.js` routes — the route that renders `ExtensionRegistration` must match the URL loaded in the iframe (typically the index route). + +## Additional resources + +- [Step-by-step Extension Development](../extension-development/index.md) +- [UI Extensions Development Flow](../../../guides/development-flow/index.md) +- [FAQ](../../../getting-started/faq/index.md) diff --git a/src/pages/services/contenthub/extension-development/extension-on-stage.png b/src/pages/services/contenthub/extension-development/extension-on-stage.png new file mode 100644 index 00000000..99ba333b Binary files /dev/null and b/src/pages/services/contenthub/extension-development/extension-on-stage.png differ diff --git a/src/pages/services/contenthub/extension-development/index.md b/src/pages/services/contenthub/extension-development/index.md new file mode 100644 index 00000000..0d76ba13 --- /dev/null +++ b/src/pages/services/contenthub/extension-development/index.md @@ -0,0 +1,581 @@ +--- +title: Step-by-step Extension Development - Content Hub Extensibility +description: Build and deploy a complete Content Hub UI extension from scratch. +contributors: + - https://github.com/AdobeDocs/uix +--- + +# Step-by-step Extension Development + +This guide walks through building a complete Content Hub extension that implements all three extension surfaces — **Asset Details tab panel**, **asset card actions**, and **Selection Bar actions** — in a single extension. By the end you will have a working extension running locally, tested against a live Content Hub environment, and deployed to Stage. + +## About the extension + +The extension built in this guide adds: +- A custom **tab panel** (Extension Template) to the Asset Details dialog showing the current asset's ID +- A custom button on asset cards and collection tiles (shown in the Assets grid, inside collections, and on the Collections grid) +- A custom button in the Selection Bar for bulk operations on selected assets + +The extension demonstrates how all three surfaces are registered in a single `register()` call and how modal data is passed via URL query parameters. + +## Create a project in Adobe Developer Console + +UI Extensions are represented as projects in [Adobe Developer Console](https://developer.adobe.com/developer-console/docs/guides/). + + + +If you don't have access to Adobe Developer Console, refer to the [How to Get Access](../../../guides/get-access/index.md) guide. + +1. Sign in to [Adobe Developer Console](https://developer.adobe.com/console) with your Adobe ID. + +![Sign in to Adobe Developer Console](../../../guides/creating-project-in-dev-console/create-project-1.png) + +2. Choose your account. + +![Choose your account](../../../guides/creating-project-in-dev-console/create-project-2.png) + +3. Choose your profile or organization. + +![Choose your profile](../../../guides/creating-project-in-dev-console/create-project-3.png) + +4. Make sure you are in the correct organization (a switcher is in the top right corner). + +![Check organization](../../../guides/creating-project-in-dev-console/create-project-4.png) + +5. Click **Create new project** → **Project from template**: + +![Create project from template](../../../guides/creating-project-in-dev-console/create-project-5.png) + +And choose **App Builder**: + +![Choose "App Builder"](../../../guides/creating-project-in-dev-console/create-project-6.png) + +6. Fill in **Project Title** (display name) and **App Name** (unique identifier — cannot be changed after creation). + +![Fill the project data](../../../guides/creating-project-in-dev-console/create-project-7.png) + +After creating, you will see a project with two default workspaces: **Production** and **Stage**. Use **Stage** for development and testing before pushing to Production. + +![A new project with 2 default Workspaces](../../../guides/creating-project-in-dev-console/create-project-8.png) + +## Set up local environment + +Make sure you have the correct Node.js version and the latest AIO CLI installed. + +```shell +$ node -v +v20.9.0 +``` + +Check your AIO CLI version: + +```shell +aio -v +``` + +Compare it with the latest published version: + +```shell +npm show @adobe/aio-cli version +``` + +If outdated, update: + +```shell +npm install -g @adobe/aio-cli +``` + +More details: [Local Environment Set Up](../../../guides/local-environment/index.md). + +## Step 1: Get the sample project + +The quickest way to start is to clone the official Content Hub sample from [adobe/aem-uix-examples](https://github.com/adobe/aem-uix-examples): + +```shell +git clone https://github.com/adobe/aem-uix-examples.git +cd aem-uix-examples/aem-contenthub-sample +npm install +``` + +Then log in to Adobe I/O and link the project to your Console workspace: + +```shell +aio login +aio console org select +aio console project select +aio console workspace select +aio app use -g +``` + +See [Code Generation](../code-generation/index.md) for the full setup walkthrough. + +## Step 2: Project structure + +After scaffolding, the relevant files are: + +```text +app.config.yaml +src/ + aem-assets-contenthub-1/ + ext.config.yaml + web-src/ + src/ + components/ + App.js + Constants.js + ExtensionRegistration.js + PanelAssetDetailsExtensionTab.js + CardActionModal.js + SelectionBarModal.js +``` + +```yaml +# app.config.yaml +extensions: + aem/assets/contenthub/1: + $include: src/aem-assets-contenthub-1/ext.config.yaml +``` + +## Step 3: Wire up routing in `App.js` + +`App.js` maps URL hash paths to React components. Each modal or panel is a separate route. Content Hub loads the extension in an iframe and navigates between routes to render panels and dialogs. + +```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 PanelAssetDetailsExtensionTab from './PanelAssetDetailsExtensionTab'; +import CardActionModal from './CardActionModal'; +import SelectionBarModal from './SelectionBarModal'; + +function App() { + return ( + + + + } /> + } /> + } /> + } /> + } /> + {/* YOUR CUSTOM ROUTES SHOULD BE HERE */} + + + + ); + + function onError(e, componentStack) {} + function fallbackComponent({ componentStack, error }) { + return ( + +

Extension rendering error

+
{componentStack + '\n' + error.message}
+
+ ); + } +} + +export default App; +``` + +## Step 4: Register all surfaces in `ExtensionRegistration.js` + +This is the core of the extension. `ExtensionRegistration.js` calls `register()` from `@adobe/uix-guest`, which connects to Content Hub and announces the extension's capabilities. + +All three namespaces (`assetDetails`, `card`, `selectionBar`) are declared in a single `register()` call. Use `let` for `guestConnection` so that click handlers defined inside `card` and `selectionBar` can close over it after `register()` resolves. + +For production, populate `allowedRepos` to restrict the extension to your specific Content Hub repository. Leave it empty during development to allow any repository. + +```js +import React from 'react'; +import { Text } from '@adobe/react-spectrum'; +import { register } from '@adobe/uix-guest'; +import { extensionId } from './Constants'; + +// Populate before deploying to Production. Empty = allow any repo (safe for dev). +const allowedRepos = []; + +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 Skipped registration: repo not in allowedRepos; + } + + const init = async () => { + let guestConnection = await register({ + id: extensionId, + methods: { + + // ── Asset Details tab panel ────────────────────────────────────── + assetDetails: { + getTabPanels() { + return [ + { + id: 'extension-template', + tooltip: 'Extension Template', + icon: 'Extension', + title: 'Extension Template', + contentUrl: '/#asset-details-extension-tab', + }, + ]; + }, + }, + + // ── Asset card & collection tile actions ───────────────────────── + // A single button configuration and a single modal serve both asset + // cards and collection tiles — `resourceType` tells the modal which + // kind of resource was clicked. + card: { + getActionButtons(actionContext) { + const { context } = actionContext || {}; + if (context !== 'assets' && context !== 'collection' && context !== 'collections') { + return []; + } + return [{ id: 'customId', label: 'Custom label', icon: 'Form' }]; + }, + async onActionClick(resourceType, buttonId, resourceId, actionContext) { + if (buttonId === 'customId') { + await guestConnection.host.modal.openDialog({ + title: 'Custom Dialog', + contentUrl: `/#card-action-modal?resourceId=${encodeURIComponent(resourceId)}&resourceType=${encodeURIComponent(resourceType)}`, + type: 'modal', + size: 'M', + }); + } + }, + }, + + // ── Selection Bar bulk actions ──────────────────────────────────── + selectionBar: { + getActionButtons(actionContext) { + const { context } = actionContext || {}; + if (context !== 'assets') return []; + return [{ id: 'customId', label: 'Custom label', icon: 'Form' }]; + }, + async onActionClick(buttonId, assetIds) { + if (buttonId === 'customId') { + await guestConnection.host.modal.openDialog({ + title: `Custom Dialog (${assetIds.length} asset${assetIds.length !== 1 ? 's' : ''} selected)`, + contentUrl: `/#selection-bar-modal?assetIds=${encodeURIComponent(JSON.stringify(assetIds))}`, + type: 'modal', + size: 'M', + }); + } + }, + }, + + }, + }); + }; + + init().catch(console.error); + return IFrame for integration with Host (Content Hub)...; +} + +export default ExtensionRegistration; +``` + +## Step 5: Build the tab panel — `PanelAssetDetailsExtensionTab.js` + +The tab panel is a separate component rendered inside an iframe when the user clicks the custom tab. It calls `attach()` to connect to Content Hub and retrieves the current asset. + +`getCurrentAsset()` is asynchronous and returns the asset id as a plain string (the asset URN). Wrap it as `{ id: assetId }` if your downstream code expects an object; otherwise use it directly. + +```js +import React, { useState, useEffect } from 'react'; +import { attach } from '@adobe/uix-guest'; +import { + Provider, defaultTheme, View, Heading, Text, Button, Divider, ProgressCircle, +} from '@adobe/react-spectrum'; +import { extensionId } from './Constants'; + +export default function PanelAssetDetailsExtensionTab() { + const [guestConnection, setGuestConnection] = useState(null); + const [asset, setAsset] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + (async () => { + try { + const connection = await attach({ id: extensionId }); + setGuestConnection(connection); + + // getCurrentAsset() returns the asset id as a plain string (e.g. "urn:aaid:aem:..."). + const assetId = await connection.host.assetDetails.getCurrentAsset(); + setAsset({ id: assetId }); + } finally { + setLoading(false); + } + })(); + }, []); + + if (loading) { + return ( + + + + + + ); + } + + return ( + + + Extension Template + + {asset && ( + + Asset ID: + + + {asset.id} + + + + )} + + + + ); +} +``` + +## Step 6: Build the card action modal — `CardActionModal.js` + +The card action modal is opened when the user clicks a custom card button. Data (the asset or collection ID and resource type) is passed via URL query parameters embedded in `contentUrl` and read from the URL hash inside the dialog component. + +Note that `payload` will be `null` on the very first render and set on the second render after the URL params are parsed — show a spinner until it's ready. + +```js +import React, { useState, useEffect } from 'react'; +import { attach } from '@adobe/uix-guest'; +import { + Provider, defaultTheme, View, Heading, Text, Button, ButtonGroup, Divider, 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 () => { + const connection = await attach({ id: extensionId }); + setGuestConnection(connection); + + // Data is passed via URL query parameters embedded in contentUrl. + const params = new URLSearchParams(window.location.hash.split('?')[1] || ''); + setPayload({ + resourceId: params.get('resourceId'), + resourceType: params.get('resourceType'), + }); + })(); + }, []); + + if (!payload) { + return ( + + + + + + ); + } + + return ( + + + Custom Dialog + + + Type: {payload.resourceType} + + + {payload.resourceId} + + + + + + + + + + ); +} +``` + +## Step 7: Build the selection bar modal — `SelectionBarModal.js` + +The selection bar modal receives a list of selected asset IDs passed as a JSON-encoded URL parameter. Decode and parse them inside the component. + +```js +import React, { useState, useEffect } from 'react'; +import { attach } from '@adobe/uix-guest'; +import { + Provider, defaultTheme, View, Heading, Text, Button, ButtonGroup, Divider, + ListView, Item, ProgressCircle, +} from '@adobe/react-spectrum'; +import { extensionId } from './Constants'; + +export default function SelectionBarModal() { + const [guestConnection, setGuestConnection] = useState(null); + const [payload, setPayload] = useState(null); + + useEffect(() => { + (async () => { + const connection = await attach({ id: extensionId }); + setGuestConnection(connection); + + // assetIds are passed as a JSON-encoded URL parameter. + const params = new URLSearchParams(window.location.hash.split('?')[1] || ''); + const raw = params.get('assetIds'); + setPayload({ assetIds: raw ? JSON.parse(raw) : [] }); + })(); + }, []); + + if (!payload) { + return ( + + + + + + ); + } + + return ( + + + Custom Dialog — {payload.assetIds.length} asset(s) selected + + ({ id, name: id }))} + height="size-2400" + aria-label="Selected assets" + > + {(item) => ( + + {item.name} + + )} + + + + + + + + ); +} +``` + +## Step 8: Test locally + +Run the extension and load it in Content Hub. See [Troubleshooting](../debug/index.md) for certificate setup and common issues. + +```shell +aio app run +``` + +```shell +For a developer preview of your UI extension in the Content Hub environment, follow the URL: + -> https://experience.adobe.com/aem/extension-manager/preview/ + +To view your local application: + -> https://localhost:9080 +press CTRL+C to terminate dev environment +``` + +Open Content Hub with the `ext=` parameter to load your locally running extension: + +```text +https://experience.adobe.com/?devMode=true&ext=https://localhost:9080#/assets/contenthub/ +``` + +You may need to accept the self-signed certificate first — see [Accept the Certificate](../debug/index.md#accept-the-certificate). + +## Step 9: Run on Stage + +After development is complete, test on Stage before deploying to Production. First, ensure you are logged into the correct org and using the Stage workspace: + +```shell +$ aio where + +You are currently in: +1. Org: My Org +2. Project: my-contenthub-extension +3. Workspace: Stage +``` + +Then deploy: + +```shell +aio app deploy +✔ Building web assets for 'aem/assets/contenthub/1' +✔ Deploying web assets for 'aem/assets/contenthub/1' +To view your deployed application: + -> https://123456-yournamespace-stage.adobeio-static.net/index.html +For a developer preview of your UI extension in the Content Hub environment, follow the URL: + -> https://experience.adobe.com/aem/extension-manager/preview/ +New Extension Point(s) in Workspace 'Stage': 'aem/assets/contenthub/1' +Successful deployment 🏄 +``` + +Use the staging deployment URL with the `ext=` parameter to verify the extension in Content Hub: + +```text +https://experience.adobe.com/?devMode=true&ext=https://123456-yournamespace-stage.adobeio-static.net/index.html#/assets/contenthub/ +``` + +![Extension running on Stage](extension-on-stage.png) + +## Step 10: Deploy to Production + +After testing on Stage, populate `allowedRepos` in `ExtensionRegistration.js` with your production Content Hub repository hostname: + +```js +const allowedRepos = [ + 'delivery-p12345-e167890.adobeaemcloud.com', +]; +``` + +Then redeploy using the Production workspace and publish through the Extension Manager. + +Refer to [UI Extensions Development Flow](../../../guides/development-flow/index.md#deploy-on-production) for the full publication and approval process. + +## Additional resources + +- [Common Concepts](../api/commons/index.md) +- [Card Actions](../api/card-actions/index.md) +- [Asset Details Tab Panels](../api/asset-details/index.md) +- [Selection Bar Actions](../api/selection-bar/index.md) +- [Code Generation](../code-generation/index.md) +- [Troubleshooting](../debug/index.md) +- [UI Extensions Development Flow](../../../guides/development-flow/index.md) +- [UI Extensions Management](../../../guides/publication/index.md) diff --git a/src/pages/services/contenthub/index.md b/src/pages/services/contenthub/index.md new file mode 100644 index 00000000..52260eb6 --- /dev/null +++ b/src/pages/services/contenthub/index.md @@ -0,0 +1,14 @@ +--- +title: Content Hub Extensibility +description: Extend Content Hub with custom UI panels, tab panels, card actions, and bulk-action buttons using Adobe App Builder and the UIX SDK. +contributors: + - https://github.com/AdobeDocs/uix +--- + +# Content Hub Extensibility + +Content Hub supports UI extensibility through [Adobe App Builder](https://developer.adobe.com/app-builder/docs/overview/) and the [UIX SDK](https://developer.adobe.com/uix/docs/). Extensions add custom surfaces to Content Hub without modifying its source code. + +All Content Hub extensions use the **`aem/assets/contenthub/1`** extension point. + +In this section, you will find the available [extension points](api/index.md) and examples of how to utilize them.