Bulkeigenschap bijwerken, voorbeeldextensie
Dit voorbeeld AEM de uitbreiding van de Console van het Fragment van de Inhoud is een uitbreiding van de actiebardie bulkupdates een bezit van het Fragment van de Inhoud aan een gemeenschappelijke waarde.
De functionele stroom van de voorbeeldextensie is als volgt:
{align="center"}
- Selecteer de Fragmenten van de Inhoud en het klikken van de knoop van de uitbreiding in de actiebaropent modaal.
- modaaltoont een vorm van de douanetoevoer die met wordt gebouwd Reageer Spectrum.
- Het voorleggen van de vorm verzendt de lijst van geselecteerde Fragmenten van de Inhoud, en de AEM gastheer aan de actie van douaneAdobe I/O Runtime.
- De actie van Adobe I/O Runtimebevestigt de input en doet de verzoeken van de PUT van HTTP om de geselecteerde Fragments van de Inhoud bij te werken.
- Een reeks HTTP-PUTTEN voor elk inhoudsfragment om de opgegeven eigenschap bij te werken.
- AEM as a Cloud Service gaat door met de eigenschappenupdates voor het inhoudsfragment en retourneert een geslaagde of mislukte reactie op de Adobe I/O Runtime-actie.
- Het modaal ontving de reactie van de actie van Adobe I/O Runtime, en toont een lijst van succesvolle bulkupdates.
Extensiepunt
In dit voorbeeld wordt het uitbreidingspunt actionBar
uitgebreid om een aangepaste knop toe te voegen aan de Content Fragment Console.
Voorbeeldextensie
In het voorbeeld wordt een bestaand Adobe Developer Console-project gebruikt en worden de volgende opties gebruikt bij het initialiseren van de App Builder-app via aio app init
.
-
Welke sjablonen wilt u zoeken?:
All Extension Points
-
Kies de sjabloon of sjablonen die u wilt installeren:
@adobe/aem-cf-admin-ui-ext-tpl
-
Wat wilt u de extensie een naam geven?:
Bulk property update
-
Geef een korte beschrijving van uw extensie op:
An example action bar extension that bulk updates a single property one or more content fragments.
-
Met welke versie wilt u beginnen?:
0.0.1
-
Wat wilt u nu doen?
-
Add a custom button to Action Bar
- Geef een labelnaam op voor de knop:
Bulk property update
- Moet u een modaal voor de knoop tonen?
y
- Geef een labelnaam op voor de knop:
-
Add server-side handler
- Met Adobe I/O Runtime kunt u naar wens serverloze code aanroepen. Hoe wilt u deze handeling een naam geven?:
generic
- Met Adobe I/O Runtime kunt u naar wens serverloze code aanroepen. Hoe wilt u deze handeling een naam geven?:
-
De gegenereerde App Builder-extensie-app wordt bijgewerkt zoals hieronder wordt beschreven.
Toepassingsroutes app-routes
src/aem-cf-console-admin-1/web-src/src/components/App.js
bevat de React router.
Er zijn twee logische reeksen routes:
-
De eerste routekaarten verzoeken aan
index.html
, die de component van het Reageren verantwoordelijk voor de uitbreidingsregistratieaanhaalt.code language-javascript <Route index element={<ExtensionRegistration />} />
-
De tweede reeks routes brengt URLs in kaart om componenten te Reageren die de inhoud van de modaal van de uitbreiding teruggeven. De
:selection
-param vertegenwoordigt een pad met een als scheidingsteken weergegeven inhoudsfragment.Als de uitbreiding veelvoudige knopen heeft om discrete acties aan te halen, elke uitbreidingsregistratiekaarten aan een hier bepaalde route.
code language-javascript <Route exact path="content-fragment/:selection/bulk-property-update" element={<BulkPropertyUpdateModal />} />
Registratie van extensies
ExtensionRegistration.js
, toegewezen aan de index.html
-route, is het ingangspunt voor de AEM extensie en definieert:
- De locatie van de extensieknop wordt weergegeven in de AEM (
actionBar
ofheaderMenu
) - De definitie van de extensieknop in de functie
getButtons()
- De klikhandler voor de knop, in de functie
onClick()
src/aem-cf-console-admin-1/web-src/src/components/ExtensionRegistration.js
import React from "react";
import { generatePath } from "react-router";
import { Text } from "@adobe/react-spectrum";
import { register } from "@adobe/uix-guest";
import { extensionId } from "./Constants";
function ExtensionRegistration() {
const init = async () => {
const guestConnection = await register({
id: extensionId, // Some unique ID for the extension used to facilitate communication between the extension and Content Fragment Console
methods: {
// Configure your Action Bar button here
actionBar: {
getButtons() {
return [
{
id: "examples.action-bar.bulk-property-update", // Unique ID for the button
label: "Bulk property update", // Button label
icon: "OpenIn", // Button icon; get name from: https://spectrum.adobe.com/page/icons/ (Remove spaces, keep uppercase)
// Click handler for the extension button
onClick(selections) {
// Collect the selected content fragment paths
const selectionIds = selections.map(
(selection) => selection.id
);
// Create a URL that maps to the
const modalURL =
"/index.html#" +
generatePath(
"/content-fragment/:selection/bulk-property-update",
{
// Set the :selection React route parameter to an encoded, delimited list of paths of the selected content fragments
selection: encodeURIComponent(selectionIds.join("|")),
}
);
// Open the route in the extension modal using the constructed URL
guestConnection.host.modal.showUrl({
// The modal title
title: "Bulk property update",
url: modalURL,
});
},
},
];
},
},
},
});
};
init().catch(console.error);
return <Text>IFrame for integration with Host (AEM)...</Text>;
}
export default ExtensionRegistration;
Modal
Elke route van de extensie, zoals gedefinieerd in App.js
, wordt toegewezen aan een component React die wordt weergegeven in het modale gebied van de extensie.
In deze voorbeeld-app is er een modale React-component (BulkPropertyUpdateModal.js
) met drie statussen:
- Laden. De gebruiker moet wachten
- Het formulier voor updates van eigenschappen met opsommingstekens waarmee de gebruiker de naam en de waarde van de eigenschap kan opgeven voor bijwerken
- De reactie van de bulkeigenschapsupdate-bewerking, waarin de inhoudsfragmenten worden vermeld die zijn bijgewerkt en de fragmenten die niet konden worden bijgewerkt
Belangrijk, zou om het even welke interactie met AEM van de uitbreiding aan een actie van Adobe I/O Runtime AppBuildermoeten worden gedelegeerd, die een afzonderlijk serverless proces is dat in Adobe I/O Runtimeloopt.
Het gebruik van Adobe I/O Runtime-acties om te communiceren met AEM is om problemen met de connectiviteit tussen bronnen van verschillende oorsprong (CORS) te voorkomen.
Wanneer het BulkVorm van de Update van het Bezit wordt voorgelegd, roept een douane onSubmitHandler()
de actie van Adobe I/O Runtime aan, die de huidige AEM (domein) en het AEM van de gebruiker toegangstoken overgaat, die beurtelings AEM het Fragment API van de Inhoudroept om de inhoudsfragmenten bij te werken.
Wanneer de reactie van de actie van Adobe I/O Runtime wordt ontvangen, wordt modal bijgewerkt om de resultaten van de bulkbezitsupdate verrichting te tonen.
src/aem-cf-console-admin-1/web-src/src/components/BulkPropertyUpdateModal.js
import React, { useState, useEffect } from 'react'
import { attach } from "@adobe/uix-guest"
import {
Flex,
Form,
Provider,
Content,
defaultTheme,
ContextualHelp,
Text,
TextField,
ButtonGroup,
Button,
Heading,
ListView,
Item,
} from '@adobe/react-spectrum'
import Spinner from "./Spinner"
import { useParams } from "react-router-dom"
import allActions from '../config.json'
import actionWebInvoke from '../utils'
import { extensionId } from "./Constants"
export default function BulkPropertyUpdateModal() {
// Set up state used by the React component
const [guestConnection, setGuestConnection] = useState()
const [actionInvokeInProgress, setActionInvokeInProgress] = useState(false);
const [actionResponse, setActionResponse] = useState();
const [propertyName, setPropertyName] = useState(null);
const [propertyValue, setPropertyValue] = useState(null);
const [validationState, setValidationState] = useState({});
// Get the selected content fragment paths from the route parameter `:selection`
let { selection } = useParams();
let fragmentIds = selection?.split('|') || [];
console.log('Content Fragment Ids', fragmentIds);
if (!fragmentIds || fragmentIds.length === 0) {
console.error("Unable to locate a list of Content Fragments to update.")
return;
}
// Asynchronously attach the extension to AEM, we must wait or the guestConnection to be set before doing anything in the modal
useEffect(() => {
(async () => {
// extensionId is the unique id of this extension (you can make this up as long as its unique) .. in this case its `bulk-property-update` pulled out into Constants.js as it is also referenced in ExtensionRegistration.js
const guestConnection = await attach({ id: extensionId })
setGuestConnection(guestConnection);
})()
}, [])
// Determine view to display in the modal
if (!guestConnection) {
// If the guestConnection is not initialized, display a loading spinner
return <Spinner />
} else if (actionInvokeInProgress) {
// If the bulk property action has been invoked but not completed, display a loading spinner
return <Spinner />
} else if (actionResponse) {
// If the bulk property action has completed, display the response
return renderResponse();
} else {
// Else display the bulk property update form
return renderForm();
}
/**
* Renders the Bulk Property Update form.
* This form has two fields:
* - Property Name: The name of the Content Fragment property name to update
* - Property Value: the value the Content Fragment property, specified by the Property Name, will be updated to
*
* @returns the Bulk Property Update form
*/
function renderForm() {
return (
// Use React Spectrum components to render the form
<Provider theme={defaultTheme} colorScheme='light'>
<Content width="100%">
<Flex width="100%">
<Form
width="100%">
<TextField label="Property name"
isRequired={true}
validationState={validationState?.propertyName}
onChange={setPropertyName}
contextualHelp={
<ContextualHelp>
<Heading>Need help?</Heading>
<Content>
<Text>The <strong>Property name</strong> must be a valid for the Content Fragment model(s) the selected Content Fragments implement.</Text>
</Content>
</ContextualHelp>
} />
<TextField
label="Property value"
validationState={validationState?.propertyValue}
onChange={setPropertyValue} />
<ButtonGroup align="start" marginTop="size-200">
<Button variant="cta" onPress={onSubmitHandler}>Update {fragmentIds.length} Content Fragments</Button>
</ButtonGroup>
</Form>
</Flex>
{/* Render the close button so the user can close the modal */}
{renderCloseButton()}
</Content>
</Provider>
)
}
/**
* Display the response from the Adobe I/O Runtime action in the modal.
* This includes:
* - A list of content fragments that were updated successfully
* - A list a content fragments that failed to update
*
* @returns the response view
*/
function renderResponse() {
// Separate the successful and failed content fragments updates
const successes = actionResponse.filter(item => item.status === 200);
const failures = actionResponse.filter(item => item.status !== 200);
return (
<Provider theme={defaultTheme} colorScheme='light'>
<Content width="100%">
<Text>Bulk updated property <strong>{propertyName}</strong> with value <strong>{propertyValue}</strong></Text>
{/* Render the list of content fragments that were updated successfully */}
{successes.length > 0 &&
<><Heading level="4">{successes.length} Content Fragments successfully updated</Heading>
<ListView
items={successes}
selectionMode="none"
aria-label="Successful updates"
>
{(item) => (
<Item key={item.fragmentId} textValue={item.fragmentId.split('/').pop()}>
{item.fragmentId.split('/').pop()}
</Item>
)}
</ListView></>}
{/* Render the list of content fragments that failed to update */}
{failures.length > 0 &&
<><Heading level="4">{failures.length} Content Fragments failed to update</Heading><ListView
items={failures}
selectionMode="none"
aria-label="Failed updates"
>
{(item) => (
<Item key={item.fragmentId} textValue={item.fragmentId.split('/').pop()}>
{item.fragmentId.split('/').pop()}
</Item>
)}
</ListView></>}
{/* Render the close button so the user can close the modal */}
{renderCloseButton()}
</Content>
</Provider>);
}
/**
* Provide a close button for the modal, else it cannot be closed (without refreshing the browser window)
*
* @returns a button that closes the modal.
*/
function renderCloseButton() {
return (
<Flex width="100%" justifyContent="end" alignItems="center" marginTop="size-400">
<ButtonGroup align="end">
<Button variant="primary" onPress={() => guestConnection.host.modal.close()}>Close</Button>
</ButtonGroup>
</Flex>
);
}
/**
* Handle the Bulk Property Update form submission.
* This function calls the supporting Adobe I/O Runtime action to update the selected Content Fragments, and then returns the response for display in the modal
* When invoking the Adobe I/O Runtime action, the following parameters are passed as they're used by the action to connect to AEM:
* - AEM Host to connect to
* - AEM access token to connect to AEM with
* - The list of Content Fragment paths to update
* - The Content Fragment property name to update
* - The value to update the Content Fragment property to
*
* @returns a list of content fragment update successes and failures
*/
async function onSubmitHandler() {
// Validate the form input fields
if (propertyName?.length > 1) {
setValidationState({propertyName: 'valid', propertyValue: 'valid'});
} else {
setValidationState({propertyName: 'invalid', propertyValue: 'valid'});
return;
}
// Mark the extension as invoking the action, so the loading spinner is displayed
setActionInvokeInProgress(true);
// Set the HTTP headers to access the Adobe I/O runtime action
const headers = {
'Authorization': 'Bearer ' + guestConnection.sharedContext.get('auth').imsToken,
'x-gw-ims-org-id': guestConnection.sharedContext.get('auth').imsOrg
};
console.log('headers', headers);
// Set the parameters to pass to the Adobe I/O Runtime action
const params = {
aemHost: `https://${guestConnection.sharedContext.get('aemHost')}`,
fragmentIds: fragmentIds,
propertyName: propertyName,
propertyValue: propertyValue
};
// Invoke the Adobe I/O Runtime action named `generic`. This name defined in the `ext.config.yaml` file.
const action = 'generic';
try {
// Invoke Adobe I/O Runtime action with the configured headers and parameters
const actionResponse = await actionWebInvoke(allActions[action], headers, params);
// Set the response from the Adobe I/O Runtime action
setActionResponse(actionResponse);
console.log(`Response from ${action}:`, actionResponse)
} catch (e) {
// Log and store any errors
console.error(e)
}
// Set the action as no longer being invoked, so the loading spinner is hidden
setActionInvokeInProgress(false);
}
}
Adobe I/O Runtime-actie
Een App Builder-app met AEM extensie kan 0 of veel Adobe I/O Runtime-acties definiƫren of gebruiken.
De acties van de Adobe Runtime zouden verantwoordelijk werk moeten zijn dat interactie met AEM, of andere de Webdiensten van de Adobe vereist.
In deze voorbeeldapp is de Adobe I/O Runtime-actie - die de standaardnaam generic
gebruikt - verantwoordelijk voor:
- Een reeks HTTP-aanvragen indienen bij de AEM Content Fragment API om de inhoudsfragmenten bij te werken.
- De antwoorden van deze HTTP-aanvragen verzamelen, deze sorteren in successen en mislukkingen
- Het terugkeren van de lijst van successen en mislukking voor vertoning door modal (
BulkPropertyUpdateModal.js
)
src/aem-cf-console-admin-1/actions/generic/index.js
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 = [ 'aemHost', 'fragmentIds', 'propertyName', 'propertyValue' ]
const requiredHeaders = ['Authorization']
const errorMessage = checkMissingRequestInputs(params, requiredParams, requiredHeaders)
if (errorMessage) {
// return and log client errors
return errorResponse(400, errorMessage, logger)
}
const body = {
"properties": {
"elements": {
[params.propertyName]: {
"value": params.propertyValue
}
}
}
};
// Extract the user Bearer token from the Authorization header used to authenticate the request to AEM
const accessToken = getBearerToken(params);
let results = await Promise.all(params.fragmentIds.map(async (fragmentId) => {
logger.info(`Updating fragment ${fragmentId} with property ${params.propertyName} and value ${params.propertyValue}`);
const res = await fetch(`${params.aemHost}${fragmentId.replace('/content/dam/', '/api/assets/')}.json`, {
method: 'put',
body: JSON.stringify(body),
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
});
if (res.ok) {
logger.info(`Successfully updated ${fragmentId}`);
return { fragmentId, status: res.status, statusText: res.statusText, body: await res.json() };
} else {
logger.info(`Failed to update ${fragmentId}`);
return { fragmentId, status: res.status, statusText: res.statusText, body: await res.text() };
}
}));
const response = {
statusCode: 200,
body: results
};
logger.info('Adobe I/O Runtime action response', response);
// Return the response to the A
return response;
} catch (error) {
// log any server errors
logger.error(error)
// return with 500
return errorResponse(500, 'server error', logger)
}
}
exports.main = main