From 52ada40e898d764437fce61fd94794808ec7c1b5 Mon Sep 17 00:00:00 2001 From: Sakshi Bhardwaj Date: Mon, 16 Feb 2026 20:06:02 +0530 Subject: [PATCH 1/2] aem-contenthub-assets-details-workfront --- .../README.md | 143 +++++++++++ .../package.json | 4 + .../actions/generic/index.js | 233 ++++++++++++++---- .../actions/token-exchange.js | 204 +++++++++++++++ .../ext.config.yaml | 15 ++ .../web-src/src/components/App.js | 2 + .../src/components/ExtensionRegistration.js | 7 + .../src/components/PanelExtendExpiryTab.js | 179 ++++++++++++++ 8 files changed, 733 insertions(+), 54 deletions(-) create mode 100644 aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/actions/token-exchange.js create mode 100644 aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/web-src/src/components/PanelExtendExpiryTab.js diff --git a/aem-contenthub-assets-details-sample/README.md b/aem-contenthub-assets-details-sample/README.md index 30b9b67..e94eed5 100644 --- a/aem-contenthub-assets-details-sample/README.md +++ b/aem-contenthub-assets-details-sample/README.md @@ -79,3 +79,146 @@ and make sure you have the below config added } } ``` + +## Workfront Integration + +This sample adds a Content Hub side panel that can create a Workfront task and link the currently selected asset to that task. + +- **UI panel**: `src/aem-contenthub-assets-details-1/web-src/src/components/PanelWorkfrontExtensionTab.js` +- **Backend action**: `src/aem-contenthub-assets-details-1/actions/generic/index.js` + +### Prerequisites + +**App Builder & Local Setup** + +- Access to Adobe Developer Console in the correct IMS organization +- App Builder entitlement for your org +- Node.js and npm installed locally +- Adobe I/O CLI (aio) installed globally +- A GitHub Personal Access Token (required when initializing from Content Hub sample repository) +- Familiarity with JavaScript/TypeScript, React, and basic REST APIs + +**Content Hub UI Extension** + +- Content Hub enabled for your AEM as a Cloud Service environment +- Permissions to access Content Hub + +**Workfront Integration** + +- A Workfront environment +- A technical integration or API client capable of server-side authentication +- Permissions to create tasks and link external documents +- A Workfront admin to configure the document provider for AEM/Content Hub + +### Step-by-Step Setup + +1. To enable secure server-to-server communication between your Content Hub extension, Adobe I/O Runtime, and Workfront, configure IMS Technical Account. + - Go to Admin Console. + - Navigate to Products → Workfront → Workfront link. + - Add the Technical Account as both User and Admin. + - Ensure your own user is also added as both User and Admin in Admin Console and in Workfront. +2. In Adobe Developer Console, create or verify a Server-to-Server (JWT) integration for Workfront. + Collect the following values from Service Credentials: + - `IMS_ENDPOINT` + - `METASCOPES` + - `TECHNICAL_ACCOUNT_CLIENT_ID` + - `TECHNICAL_ACCOUNT_CLIENT_SECRET` + - `TECHNICAL_ACCOUNT_EMAIL` + - `TECHNICAL_ACCOUNT_ID` + - `ORGANIZATION_ID` + - `PRIVATE_KEY` + - `PUBLIC_KEY` +3. In Workfront, confirm API access and required permissions. + - Note your tenant base URL for `WORKFRONT_BASE_URL` (e.g., `https://.my.workfront.com/attask/api/v15.0`). + - Ensure a default project exists and note its `DEFAULT_PROJECT_ID`. + - If using AEM external documents, get the `DOCUMENT_PROVIDER_ID` from Setup → Documents → External Document Providers. + - Ensure permissions to create tasks in the default project and to link external documents. + - Get Workfront user ID (used by DOCUMENT_PROVIDER_ID API when needed): + + ```bash + curl --location 'https://.my.workfront.com/attask/api-internal/user/realUser' \ + --header 'Authorization: Bearer ' + # Response → use ID field as USER_ID in DOCUMENT_PROVIDER_ID API + ``` + + - One-time creation of DOCUMENT_PROVIDER_ID via API (optional): + + ```bash + curl --location --request PUT \ + "https://.my.workfront.com/attask/api/unsupported/user/?action=initializeStatelessDocumentProviderForUser" \ + --header 'user-agent: Workfront Fusion/production' \ + --header 'content-type: application/json' \ + --header 'authorization: Bearer ' \ + --data '{ + "providerType": "AEM", + "documentProviderConfigID": "", + "documentProviderConfigName": "Content Hub" + }' + ``` + + - Replace `` with the Workfront user ID. + - Replace `` with the ID of the active AEM provider configuration in Workfront. + - The resulting configuration ID is what you set as `DOCUMENT_PROVIDER_ID` in your `.env`. +4. All secrets must be added to your App Builder workspace and provided via `.env`. + These are read by the Runtime action via + `ext.config.yaml -> runtimeManifest.packages.aem-contenthub-assets-details-1.actions.generic.inputs`. + + In Stage and Production, provide all secrets via `.env` files so that `aio` can inject them at build and deploy time. + + **Required Environment Variables** + + ```bash + + # IMS / Technical Account + IMS_ENDPOINT=ims-na1.adobelogin.com + METASCOPES= + TECHNICAL_ACCOUNT_CLIENT_ID= + TECHNICAL_ACCOUNT_CLIENT_SECRET= + TECHNICAL_ACCOUNT_EMAIL= + TECHNICAL_ACCOUNT_ID= + ORGANIZATION_ID= + PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n" + PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n" + CERTIFICATE_EXPIRATION_DATE= + + # Workfront + WORKFRONT_BASE_URL=https://.my.workfront.com/attask/api/v15.0 + AEM_AUTHOR= + DOCUMENT_PROVIDER_ID= + DEFAULT_PROJECT_ID= + + # Runtime Namespace + AIO_runtime_namespace= + ``` + + Verify namespace: + + ```bash + aio runtime namespace get + ``` + + When selecting a workspace using the Adobe I/O CLI, the namespace is automatically populated in `.env`. + +5. Edit `src/aem-contenthub-assets-details-1/web-src/src/components/ExtensionRegistration.js` and update `allowedRepos` to include your Delivery host. +6. Run locally with `aio app run` and verify the Workfront panel in Content Hub. Deploy using `aio app deploy`. + +### Why Runtime Namespace Is Required + +- The UI constructs the backend URL using the namespace to reach your deployed web action, e.g. `https://.adobeio-static.net/api/v1/web/aem-contenthub-assets-details-1/generic`. +- This ensures requests route to the correct Adobe I/O Runtime workspace (dev/stage/prod). If the namespace is wrong or missing, calls 404 or hit the wrong environment. +- You can see this used in `src/aem-contenthub-assets-details-1/web-src/src/components/PanelWorkfrontExtensionTab.js` where `backendUrl` is built from `process.env.AIO_runtime_namespace`. +- Ensure your Workfront API version in `WORKFRONT_BASE_URL` matches your tenant (the example uses `v15.0`). + +### How Flow Works + +1. The UI tab gathers Task Name/Description and calls the web action with `action: "createTaskAndLinkAsset"` and the current asset ID. +2. The action exchanges a JWT for an IMS access token using your integration. +3. It creates a Workfront task (project = `DEFAULT_PROJECT_ID`). +4. It links the current asset to that task using `DOCUMENT_PROVIDER_ID` and `AEM_AUTHOR`. + +### Troubleshooting + +- **Missing env vars**: The action will return 500 with a message listing missing configuration elements. +- **401/403 from Workfront**: Verify `METASCOPES`, IMS integration credentials, and `WORKFRONT_BASE_URL`. +- **Unknown action**: Ensure the UI sends `action: createTaskAndLinkAsset` and your action is deployed. +- **Panel hidden**: Make sure your repo host is in `allowedRepos` in `ExtensionRegistration.js`. diff --git a/aem-contenthub-assets-details-sample/package.json b/aem-contenthub-assets-details-sample/package.json index 673be92..3d93595 100644 --- a/aem-contenthub-assets-details-sample/package.json +++ b/aem-contenthub-assets-details-sample/package.json @@ -11,9 +11,13 @@ "@adobe/uix-guest": "^0.10.5", "@react-spectrum/list": "^3.0.0-rc.0", "@spectrum-icons/workflow": "^3.2.0", + "axios": "^1.6.0", + "https-proxy-agent": "^7.0.2", "chalk": "^4", "core-js": "^3.6.4", + "jsonwebtoken": "^9.0.2", "node-html-parser": "^5.4.2-0", + "qs": "^6.11.2", "react": "^16.13.1", "react-dom": "^16.13.1", "react-error-boundary": "^1.2.5", diff --git a/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/actions/generic/index.js b/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/actions/generic/index.js index 6523bf2..549533c 100644 --- a/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/actions/generic/index.js +++ b/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/actions/generic/index.js @@ -2,68 +2,193 @@ * */ +const { Core } = require('@adobe/aio-sdk') +const { checkMissingRequestInputs } = require('../utils') +const axios = require('axios') +const { getAccessToken } = require('./token-exchange'); + /** - * This is a sample action showcasing how to access an external API - * - * Note: - * You might want to disable authentication and authorization checks against Adobe Identity Management System for a generic action. In that case: - * - Remove the require-adobe-auth annotation for this action in the manifest.yml of your application - * - Remove the Authorization header from the array passed in checkMissingRequestInputs - * - The two steps above imply that every client knowing the URL to this deployed action will be able to invoke it without any authentication and authorization checks against Adobe Identity Management System - * - Make sure to validate these changes against your security requirements before deploying the action + * Get Workfront configuration from environment variables */ +function getWorkfrontConfig(params) { + const requiredVars = ['WORKFRONT_BASE_URL', 'AEM_AUTHOR', 'DOCUMENT_PROVIDER_ID']; + + const missingVars = requiredVars.filter(varName => !params[varName]); + + if (missingVars.length > 0) { + throw new Error(`Missing required Workfront environment variables: ${missingVars.join(', ')}`); + } + + return { + baseUrl: params.WORKFRONT_BASE_URL, + aemAuthor: params.AEM_AUTHOR, + documentProviderID: params.DOCUMENT_PROVIDER_ID + }; +} +/** + * Create a task in Workfront + */ +async function createWorkfrontTask(taskData, accessToken, params) { + const workfrontConfig = getWorkfrontConfig(params); + const taskUrl = `${workfrontConfig.baseUrl}/task`; + + const headers = { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accessToken}` + }; -const fetch = require('node-fetch') -const { Core } = require('@adobe/aio-sdk') -const { errorResponse, getBearerToken, stringParameters, checkMissingRequestInputs } = require('../utils') - -// main function that will be executed by Adobe I/O Runtime -async function main (params) { - // create a Logger - const logger = Core.Logger('main', { level: params.LOG_LEVEL || 'info' }) - - try { - // 'info' is the default level if not set - logger.info('Calling the main action') - - // log parameters, only if params.LOG_LEVEL === 'debug' - logger.debug(stringParameters(params)) - - // check for missing request input parameters and headers - const requiredParams = [/* add required params */] - const requiredHeaders = ['Authorization'] - const errorMessage = checkMissingRequestInputs(params, requiredParams, requiredHeaders) - if (errorMessage) { - // return and log client errors - return errorResponse(400, errorMessage, logger) + const response = await axios.post(taskUrl, taskData, { headers }); + + if (!response.data) { + throw new Error(`Task creation failed with status: ${response.status}`); } + + return response.data; +} - // extract the user Bearer token from the Authorization header - const token = getBearerToken(params) +/** + * Link asset to task in Workfront + */ +async function linkAssetToTask(assetId, taskID, accessToken, params) { + const workfrontConfig = getWorkfrontConfig(params); + const linkUrl = `${workfrontConfig.baseUrl}/extdoc?action=linkExternalDocumentObjects`; + + const headers = { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accessToken}` + }; - // replace this with the api you want to access - const apiEndpoint = `${params.API_ENDPOINT}` - // fetch content from external api endpoint - const res = await fetch(apiEndpoint) - if (!res.ok) { - throw new Error('request to ' + apiEndpoint + ' failed with status code ' + res.status) - } - const content = await res.json() - const response = { - statusCode: 200, - body: content + // Format the asset ID for Workfront + const encodedAssetId = encodeURIComponent(assetId); + const fullAssetId = `urn:workfront:documents:aem:${workfrontConfig.aemAuthor}:${encodedAssetId}`; + + // Create the objects string + const objectsString = JSON.stringify({ + [assetId]: { + ID: fullAssetId, + name: 'asset-test', + ext: 'jpg', + isFolder: 'false' + } + }); + + const linkData = { + objects: objectsString, + refObjCode: 'TASK', + refObjID: taskID, + documentProviderID: workfrontConfig.documentProviderID, + providerType: 'AEM' + }; + + const response = await axios.put(linkUrl, linkData, { + headers, + timeout: 30000 + }); + + return response.status === 200; +} + +/** + * Handle extend expiry request: create task and link asset + */ +async function handleCreateExtendExpiryRequest(params, logger) { + try { + // Get access token + const accessToken = await getAccessToken(params); + logger.info('Access token retrieved successfully'); + + // Create task + const taskData = { + name: params.taskName || 'Task from AEM Asset', + projectID: params.DEFAULT_PROJECT_ID, + description: params.taskDescription || 'Task created from AEM Content Hub asset', + plannedStartDate: new Date().toISOString().split('T')[0] + 'T00:00:00:000-0800', + plannedCompletionDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] + 'T00:00:00:000-0800' + }; + + const taskResult = await createWorkfrontTask(taskData, accessToken.access_token, params); + const taskID = taskResult?.data?.ID || taskResult?.taskID; + logger.info(`Created task with ID: ${taskID}`); + + // Link asset to task + let assetLinked = false; + + if (params.assetId) { + assetLinked = await linkAssetToTask( + params.assetId, + taskID, + accessToken.access_token, + params + ); + } + + return { + statusCode: 200, + body: { + success: true, + taskID: taskID, + taskCreated: true, + assetLinked: assetLinked, + message: `Task created successfully with ID: ${taskID}. Asset linking: ${assetLinked ? 'Success' : 'Failed'}` + } + }; + + } catch (error) { + logger.error('Error creating task:', error.message); + + return { + statusCode: 500, + body: { + error: `Task creation failed: ${error.message}`, + details: { + message: error.message, + status: error.response?.status + } + } + }; } +} - // log the response status code - logger.info(`${response.statusCode}: successful request`) - return response - } catch (error) { - // log any server errors - logger.error(error) - // return with 500 - return errorResponse(500, 'server error', logger) - } +/** + * Main function executed by Adobe I/O Runtime + */ +async function main(params) { + const logger = Core.Logger('main', { level: params.LOG_LEVEL || 'info' }); + + try { + // Handle preflight OPTIONS request + if (params.__ow_method === 'OPTIONS') { + return { statusCode: 200, body: {} }; + } + + // Check for required parameters + const requiredParams = ['action']; + const errorMessage = checkMissingRequestInputs(params, requiredParams, []); + if (errorMessage) { + return { + statusCode: 400, + body: { error: errorMessage } + }; + } + + // Handle the createTaskAndLinkAsset action + if (params.action === 'createTaskAndLinkAsset') { + return await handleCreateExtendExpiryRequest(params, logger); + } + + return { + statusCode: 400, + body: { error: `Unknown action: ${params.action}` } + }; + + } catch (error) { + logger.error('Server error:', error); + return { + statusCode: 500, + body: { error: 'Internal server error' } + }; + } } -exports.main = main +exports.main = main; \ No newline at end of file diff --git a/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/actions/token-exchange.js b/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/actions/token-exchange.js new file mode 100644 index 0000000..f61b143 --- /dev/null +++ b/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/actions/token-exchange.js @@ -0,0 +1,204 @@ +/* +* +*/ + +const axios = require('axios'); +const qs = require('qs'); +const jwt = require('jsonwebtoken'); +const util = require('util'); + +class IMSJWTTokenExchange { + constructor(host, proxy) { + if (host === undefined) { + throw new Error("Client lib must have a target host defined, imsHost or jilHost"); + } + this.host = host; + + if (proxy) { + const { HttpsProxyAgent } = require("https-proxy-agent"); + const httpsAgent = new HttpsProxyAgent({host: proxy.host, port: proxy.port}); + this.request = axios.create({ + baseURL: `https://${this.host}`, + timeout: 10000, + httpsAgent + }); + } else { + this.request = axios.create({ + baseURL: `https://${this.host}`, + timeout: 10000 + }); + } + + // Request interceptor for logging + this.request.interceptors.request.use(function (config) { + console.debug(`>> ${config.method} ${config.url}`); + if (config.verbose) { + console.debug(JSON.stringify(config, null, 2)); + } + return config; + }, function (error) { + console.error(`Failed making request ${error.message}`); + return Promise.reject(error); + }); + + // Response interceptor for logging + this.request.interceptors.response.use(function (response) { + console.debug(`<< ${response.config.method} ${response.config.url} ${response.status}`); + if (response.config.verbose) { + console.debug(util.inspect(response.data)); + } + return response; + }, function (error) { + if (error.config) { + console.error(`Error performing operation ${error.message} request ${error.config.url}`); + } else { + console.error(`Error performing operation ${error.message} request (no config)`); + } + if (error.response) { + console.error(util.inspect(error.response.data)); + } + return Promise.reject(error); + }); + } + + checkRequired(options, key) { + if (options[key] === undefined) { + throw new Error(`${key} is a required option.`); + } + } + + /** + * Exchanges a integration for an access token using JWT Token exchange with IMS. + * @returns { + * access_token, + * token_type, + * expires_in + * } + */ + async exchangeJwt(options) { + this.checkRequired(options, "issuer"); + this.checkRequired(options, "subject"); + this.checkRequired(options, "expiration_time_seconds"); + this.checkRequired(options, "metascope"); + this.checkRequired(options, "client_id"); + this.checkRequired(options, "client_secret"); + this.checkRequired(options, "privateKey"); + + const jwt_payload = { + iss: options.issuer, + sub: options.subject, + exp: options.expiration_time_seconds, + aud: `https://${this.host}/c/${options.client_id}` + }; + + options.metascope.forEach((v) => { + jwt_payload[`https://${this.host}/s/${v}`] = true; + }); + + // Sign with RSA256 + const jwt_token = jwt.sign(jwt_payload, options.privateKey, { algorithm: 'RS256' }); + + if (options.publicKey) { + console.debug(jwt.verify(jwt_token, options.publicKey, { complete: true })); + } + + const body = qs.stringify({ + client_id: options.client_id, + client_secret: options.client_secret, + jwt_token: jwt_token + }); + + const config = { + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + verbose: options.verbose + }; + + const response = await this.request.post(`/ims/exchange/jwt`, body, config); + if (response.status === 200) { + return response.data; + } + throw new Error("Failed to exchange jwt."); + } +} + +/** + * Deep validation helper function + */ +const assertPresent = (config, path, missing) => { + const pathElements = path.split("."); + let c = config; + for (let p of pathElements) { + if (!c[p]) { + missing.push(path); + return; + } + c = c[p]; + } +}; + +/** + * Get integration configuration from environment variables with deep validation + */ +function getIntegrationConfig(params) { + const integrationConfig = { + integration: { + imsEndpoint: params.IMS_ENDPOINT, + org: params.ORGANIZATION_ID, + id: params.TECHNICAL_ACCOUNT_ID, + technicalAccount: { + clientId: params.TECHNICAL_ACCOUNT_CLIENT_ID, + clientSecret: params.TECHNICAL_ACCOUNT_CLIENT_SECRET + }, + metascopes: params.METASCOPES, + privateKey: params.PRIVATE_KEY, + publicKey: params.PUBLIC_KEY + } + }; + + // Deep validation + const missing = []; + assertPresent(integrationConfig, "integration.imsEndpoint", missing); + assertPresent(integrationConfig, "integration.org", missing); + assertPresent(integrationConfig, "integration.id", missing); + assertPresent(integrationConfig, "integration.technicalAccount.clientId", missing); + assertPresent(integrationConfig, "integration.technicalAccount.clientSecret", missing); + assertPresent(integrationConfig, "integration.metascopes", missing); + assertPresent(integrationConfig, "integration.privateKey", missing); + assertPresent(integrationConfig, "integration.publicKey", missing); + + if (missing.length > 0) { + throw new Error(`The following configuration elements are missing: ${missing.join(", ")}`); + } + + return integrationConfig; +} + +/** + * Get access token using production-ready JWT exchange + */ +async function getAccessToken(params) { + const integrationConfig = getIntegrationConfig(params); + + // Create JWT exchange instance + const jwtExchange = new IMSJWTTokenExchange(integrationConfig.integration.imsEndpoint); + + // Exchange JWT for access token + return await jwtExchange.exchangeJwt({ + issuer: `${integrationConfig.integration.org}`, + subject: `${integrationConfig.integration.id}`, + expiration_time_seconds: Math.floor((Date.now() / 1000) + 3600 * 8), + metascope: integrationConfig.integration.metascopes.split(","), + client_id: integrationConfig.integration.technicalAccount.clientId, + client_secret: integrationConfig.integration.technicalAccount.clientSecret, + privateKey: integrationConfig.integration.privateKey, + publicKey: integrationConfig.integration.publicKey, + verbose: params.LOG_LEVEL === 'debug' + }); +} + +module.exports = { + IMSJWTTokenExchange, + getIntegrationConfig, + getAccessToken, + assertPresent +}; \ No newline at end of file diff --git a/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/ext.config.yaml b/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/ext.config.yaml index fef3b66..dedb118 100644 --- a/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/ext.config.yaml +++ b/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/ext.config.yaml @@ -18,6 +18,21 @@ runtimeManifest: inputs: LOG_LEVEL: debug API_ENDPOINT: $API_ENDPOINT + IMS_ENDPOINT: $IMS_ENDPOINT + METASCOPES: $METASCOPES + TECHNICAL_ACCOUNT_CLIENT_ID: $TECHNICAL_ACCOUNT_CLIENT_ID + TECHNICAL_ACCOUNT_CLIENT_SECRET: $TECHNICAL_ACCOUNT_CLIENT_SECRET + TECHNICAL_ACCOUNT_EMAIL: $TECHNICAL_ACCOUNT_EMAIL + TECHNICAL_ACCOUNT_ID: $TECHNICAL_ACCOUNT_ID + ORGANIZATION_ID: $ORGANIZATION_ID + PRIVATE_KEY: $PRIVATE_KEY + PUBLIC_KEY: $PUBLIC_KEY + CERTIFICATE_EXPIRATION_DATE: $CERTIFICATE_EXPIRATION_DATE + WORKFRONT_BASE_URL: $WORKFRONT_BASE_URL + AEM_AUTHOR: $AEM_AUTHOR + DOCUMENT_PROVIDER_ID: $DOCUMENT_PROVIDER_ID + DEFAULT_PROJECT_ID: $DEFAULT_PROJECT_ID + AIO_runtime_namespace: $AIO_runtime_namespace annotations: require-adobe-auth: false final: true diff --git a/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/web-src/src/components/App.js b/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/web-src/src/components/App.js index ec71c20..12bc2d2 100644 --- a/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/web-src/src/components/App.js +++ b/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/web-src/src/components/App.js @@ -7,6 +7,7 @@ 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 PanelExtendExpiryTab from './PanelExtendExpiryTab'; function App() { return ( @@ -16,6 +17,7 @@ function App() { } /> } /> } /> + } /> // YOUR CUSTOM ROUTES SHOULD BE HERE diff --git a/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/web-src/src/components/ExtensionRegistration.js b/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/web-src/src/components/ExtensionRegistration.js index bf007d2..b6e1a63 100644 --- a/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/web-src/src/components/ExtensionRegistration.js +++ b/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/web-src/src/components/ExtensionRegistration.js @@ -42,6 +42,13 @@ function ExtensionRegistration() { 'title': 'Asset Details Extension Tab', 'contentUrl': '/#asset-details-extension-tab', }, + { + 'id': 'extend-expiry-tab', + 'tooltip': 'Extend Expiry', + 'icon': 'AssetsExpired', + 'title': 'Extend Expiry', + 'contentUrl': '/#extend-expiry-tab', + }, ]; }, }, diff --git a/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/web-src/src/components/PanelExtendExpiryTab.js b/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/web-src/src/components/PanelExtendExpiryTab.js new file mode 100644 index 0000000..339cfdb --- /dev/null +++ b/aem-contenthub-assets-details-sample/src/aem-contenthub-assets-details-1/web-src/src/components/PanelExtendExpiryTab.js @@ -0,0 +1,179 @@ +/* + * + */ + +import React, { useState, useEffect } from 'react'; +import { attach } from '@adobe/uix-guest'; +import { + Flex, + Provider, + defaultTheme, + Text, + Button, + View, + TextField, + TextArea, + Heading, + StatusLight +} from '@adobe/react-spectrum'; + +import { extensionId } from './Constants'; + +/** + * Configuration constants + */ +const CONFIG = { + backendUrl: `https://${process.env.AIO_runtime_namespace}.adobeio-static.net/api/v1/web/aem-contenthub-assets-details-1/generic`, + requestTimeout: 30000 +}; + +export default function PanelExtendExpiryTab() { + const [guestConnection, setGuestConnection] = useState(); + const [currentAsset, setCurrentAsset] = useState(); + const [taskName, setTaskName] = useState(''); + const [taskDescription, setTaskDescription] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [status, setStatus] = useState({ type: 'info', message: '' }); + const [isInitialized, setIsInitialized] = useState(false); + + useEffect(() => { + if (!isInitialized) { + initializeExtension(); + } + }, [isInitialized]); + + const initializeExtension = async () => { + try { + const guestConnection = await attach({ id: extensionId }); + setGuestConnection(guestConnection); + + const asset = await guestConnection.host.assetDetails.getCurrentAsset(); + setCurrentAsset(asset); + setIsInitialized(true); + } catch (error) { + console.error('Error initializing extension:', error); + setStatus({ type: 'negative', message: 'Failed to initialize extension' }); + setIsInitialized(true); // Set to true even on error to prevent retry loops + } + }; + + const createTaskAndLinkAsset = async () => { + if (!currentAsset) { + displayToast('negative', 'Asset not available'); + return; + } + + setIsLoading(true); + setStatus({ type: 'info', message: '' }); + + try { + const response = await fetch(CONFIG.backendUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + action: 'createTaskAndLinkAsset', + assetId: currentAsset, + taskName: taskName, + taskDescription: taskDescription + }), + signal: AbortSignal.timeout(CONFIG.requestTimeout) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Backend request failed! Status: ${response.status}, Message: ${errorText}`); + } + + const result = await response.json(); + handleBackendResponse(result); + + } catch (error) { + console.error('Error creating task and linking asset:', error); + setStatus({ type: 'negative', message: `Error: ${error.message}` }); + displayToast('negative', `Error: ${error.message}`); + } finally { + setIsLoading(false); + } + }; + + const handleBackendResponse = (result) => { + const responseBody = result.body || result; + + if (responseBody && responseBody.success) { + // Clear any previous status messages + setStatus({ type: 'info', message: '' }); + // Clear the form fields + setTaskName(''); + setTaskDescription(''); + displayToast('positive', 'Task created successfully!'); + } else if (responseBody && responseBody.error) { + setStatus({ type: 'negative', message: `Error: ${responseBody.error}` }); + displayToast('negative', `Failed to create task: ${responseBody.error}`); + } else { + // Fallback for unexpected response structure + setStatus({ type: 'info', message: '' }); + // Clear the form fields + setTaskName(''); + setTaskDescription(''); + displayToast('positive', 'Task creation completed'); + } + }; + + const displayToast = (variant, message) => { + if (guestConnection) { + guestConnection.host.toast.display({ variant, message }); + } + }; + + return ( + + + + + Workfront Integration + + + + Create a Workfront task and link this asset to it. + + + + +