AEM Edge Delivery - Universal Editor Extension for Quick Links Panel


Create a Quick Links Panel in Universal Editor Right Rail to show buttons for opening a page (being edited in left pane) in Edge Delivery Preview (eg. https://main--eaem-dev-eds--schoudry.aem.page/index/level-one) or Live (https://main--eaem-dev-eds--schoudry.aem.live/index/level-one) 

Product Documentation:
                - UE Extension: https://developer.adobe.com/uix/docs/services/aem-universal-editor/api/properties-rails/




1) Create a new project in Developer Console (https://developer.adobe.com/console). Make sure you choose the App Builder template




2) Project created with default Stage and Production Workspaces...



3) Open terminal, login to aio and create an app. While going through the prompts make sure you select the Universal Editor Extension Template...

                              > aio login
                              > aio app init eaem-ue-quick-links



3) Add the following code in your extension eg. src\universal-editor-ui-1\web-src\src\components\ExperienceAEMUEQuickLinksRail.js. It makes Query Builder calls to AEM to get the necessary data for building Preview and Live links


import React, { useState, useEffect } from 'react'
import { attach } from "@adobe/uix-guest"
import {
  Provider,
  defaultTheme,
  Button,
} from '@adobe/react-spectrum'

import { extensionId } from "./Constants"

export default function ExperienceAEMUEQuickLinksRail () {
  const [guestConnection, setGuestConnection] = useState()
  const [edsSitesRoots, setEdsSitesRoots] = useState({})
  const [aemHost, setAemHost] = useState('')
  const [pagePath, setPagePath] = useState('')
  const [githubOrg, setGithubOrg] = useState('')

  const EDS_SITES_QUERY = "/bin/querybuilder.json?" +
                            "1_property=*/sling:resourceType" +
                            "&1_property.value=core/franklin/components/page/v1/page" +
                            "&2_property=*/cq:conf" +
                            "&2_property.operation=exists" +
                            "&path=/content"

  const FIND_ORG_QUERY = "/bin/querybuilder.json?1_property=owner" +
                        "&1_property.operation=exists" +
                        "&path=/conf" +
                        "&p.hits=selective" +
                        "&p.properties=owner"

  const fetchEdsSitesData = async (AEM_HOST, guestConnection) => {
    const sitesQueryUrl = `${AEM_HOST}${EDS_SITES_QUERY}`;      
    const requestOptions = {
      headers: {
        'Authorization': `Bearer ${guestConnection.sharedContext.get("token")}`
      }
    };
    const response = await fetch(sitesQueryUrl, requestOptions)
    const responseData = await response.json()

    return responseData.hits ? responseData.hits.reduce((map, hit) => {
      const value = hit.path.substring(hit.path.lastIndexOf('/') + 1);
      map[hit.path] = value;
      return map;
    }, {}) : {}
  }

  const getGithubOrg = async (AEM_HOST, guestConnection) => {
    const orgQueryUrl = `${AEM_HOST}${FIND_ORG_QUERY}`;      
    const requestOptions = {
      headers: {
        'Authorization': `Bearer ${guestConnection.sharedContext.get("token")}`
      }
    };
    const response = await fetch(orgQueryUrl, requestOptions)
    const responseData = await response.json()

    return responseData.hits ? responseData.hits[0].owner : ''
  }

  const getAemHost = (editorState) => {
    return editorState.connections.aemconnection.substring(editorState.connections.aemconnection.indexOf('xwalk:') + 6);
  }

  const getPagePath = (aemHost, location) => {
    let qIndex = location.lastIndexOf('?');
    let path = (qIndex !== -1) ? location.substring(0, qIndex) : location;
    if (path.startsWith(aemHost)) {
      path = path.substring(aemHost.length);
    }
    return path.endsWith('.html') ? path.substring(0, path.length - 5) : path;
  }

  const getPreviewOrLiveUrl = (siteRoots, pagePath, org, urlType) => {
    const branch = getRefParam();
    let site = '';
    let pageRelativePath = pagePath;
   
    for (const [path, value] of Object.entries(siteRoots)) {
      if (pagePath.startsWith(path)) {
        site = value;
        pageRelativePath = pagePath.substring(path.length);
      }
    }

    if(pageRelativePath == '/index') {
      pageRelativePath = '/';
    }

    const domain = urlType === 'PREVIEW' ? 'aem.page' : 'aem.live';
    return `https://${branch}--${site}--${org}.${domain}${pageRelativePath}`;
  }

  const getRefParam = () => {
    return new URLSearchParams(window.location.search).get('ref') || 'main';
  }

  const handleOpenPreview = () => {
    const url = getPreviewOrLiveUrl(edsSitesRoots, pagePath, githubOrg, 'PREVIEW');
    window.open(url, '_blank', 'noopener,noreferrer');
  }

  const handleOpenLive = () => {
    const url = getPreviewOrLiveUrl(edsSitesRoots, pagePath, githubOrg, 'LIVE');
    window.open(url, '_blank', 'noopener,noreferrer');
  }

  useEffect(() => {
    (async () => {
      const guestConnection = await attach({ id: extensionId })
      setGuestConnection(guestConnection);

      const editorState = await guestConnection.host.editorState.get();

      if(editorState){
        const hostValue = getAemHost(editorState);
        setAemHost(hostValue);

        const siteRoots = await fetchEdsSitesData(hostValue, guestConnection);
        const githubOrgValue = await getGithubOrg(hostValue, guestConnection);
        const pagePathValue = getPagePath(hostValue, editorState.location);

        setEdsSitesRoots(siteRoots)
        setGithubOrg(githubOrgValue)
        setPagePath(pagePathValue)
      }
    })()
  }, [])

  return (
    <Provider theme={defaultTheme} colorScheme='dark'>
      <div style={{ height: '940px',paddingTop: '20px' ,fontSize: '20px', paddingLeft: '20px' }}>
        <div style={{ textAlign: 'center', marginBottom: '20px', border: '2px solid #ccc', padding: '10px', borderRadius: '5px' }}>
          Quick Links
        </div>
        <Button variant="primary" onPress={handleOpenPreview} UNSAFE_style={{ cursor: 'pointer' }} title="Open page in preview">
          Open page in preview
        </Button>
        <div style={{ marginTop: '10px' }}>
          <Button variant="primary" onPress={handleOpenLive} UNSAFE_style={{ cursor: 'pointer' }}>
            Open page in live
          </Button>
        </div>
      </div>
    </Provider>
  )
}


4) Run app locally (if you get any dependencies errors do npm install first). The following command will host app on https://localhost:9080

                              > aio app run  

5) Before trying the extension in universal editor, open the app url https://localhost:9080 in a browser tab and accept the certificate...



6) Construct the UE editor url with the extension url added (as ?devMode=true&ext=https://localhost:9080) eg. the full url...

                              https://experience.adobe.com/#/@acsaem/aem/editor/canvas/author-p10961-e880305.adobeaemcloud.com/content/eaem-dev-eds/index.html?devMode=true&ext=https://localhost:9080

7) You'll see the following error, as AEM is not prepared to accept requests from  https://experience.adobe.com



8) Add the following in you AEM project CDN yaml config file (eg. eaem-random-test\config\cdn.yaml). It directs CDN to add the necessary Content Security Policy response header to allow author url (eg. https://author-p10961-e880305.adobeaemcloud.com/) to be iFrame'd in  https://experience.adobe.com and also relaxes the login-token cookie security config using SameSite=None so the user remains authenticated within the iframe. With the change pushed run Config Pipeline...

kind: "CDN"
version: "1"
metadata:
    envTypes: [ "rde", "dev" ]
data:
  responseTransformations:
    rules:
      - name: "frame-security-policy-for-author-dev"
        when:
            reqProperty: domain
            equals: "author-p10961-e880305.adobeaemcloud.com"
        actions:
          - type: set
            value:  frame-ancestors 'self' https://experience.adobe.com
            respHeader: Content-Security-Policy      
      - name: "set-samesite-none-for-login-token"
        when:
            respHeader: Set-Cookie
            matches: "login-token=.*"
        actions:
          - type: set
            respHeader: Set-Cookie
            value: { respHeader: Set-Cookie, transform: [ {op: 'replace', match: 'SameSite=(Lax|Strict|None)', replacement: 'SameSite=None'} ] }

9) To make Query Builder calls from your extension, we need to bypass the CORS checks, so add your extension domain (local extension, stage and production obtained in next steps) to the CORS config of AEM project (eg. eaem-random-test\ui.config\src\main\content\jcr_root\apps\eaem-random-test\osgiconfig\config.author\com.adobe.granite.cors.impl.CORSPolicyImpl~eaem-random-test.cfg.json). Run the Full Stack pipeline in Cloud Manager for deploying this config... 

{
  "alloworigin": ["http://localhost:3000", "https://localhost:9080", "https://258616-eaemuequicklinks-stage.adobeio-static.net", "https://258616-eaemuequicklinks.adobeio-static.net"],
  "allowedpaths": [".*"],
  "supportedheaders": [
    "Authorization",
    "Origin",
    "Accept",
    "X-Requested-With",
    "Content-Type",
    "Access-Control-Request-Method",
    "Access-Control-Request-Headers"
  ],
  "supportedmethods": ["GET","HEAD"],
  "alloworiginregexp": []
}


10) With above changes, load editor url again and you should see the extension...

                              https://experience.adobe.com/#/@acsaem/aem/editor/canvas/author-p10961-e880305.adobeaemcloud.com/content/eaem-dev-eds/index.html?devMode=true&ext=https://localhost:9080



11) Make sure you are in the Stage workspace, using following commands deploy the app. You should get an App Url for stage something like https://258616-eaemuequicklinks-stage.adobeio-static.net/index.html

                              > aio where
                              > aio app deploy

12) You should now be able to load the Stage Deployed Extension in your Universal Editor eg. 

                              https://experience.adobe.com/#/@acsaem/aem/editor/canvas/author-p10961-e880305.adobeaemcloud.com/content/eaem-dev-eds/index.html?devMode=true&ext=https://258616-eaemuequicklinks-stage.adobeio-static.net/index.html

13) Its time to deploy the App to Production. Switch workspace using the following commands and deploy (you should get the prod app url something like https://258616-eaemuequicklinks.adobeio-static.net/index.html)

                              > aio app use -w Production
                              > aio app deploy



14) One last check using Prod App url and lets submit it for approval to make it generally available...

                              https://experience.adobe.com/#/@acsaem/aem/editor/canvas/author-p10961-e880305.adobeaemcloud.com/content/eaem-dev-eds/index.html?devMode=true&ext=https://258616-eaemuequicklinks.adobeio-static.net/index.html


15) Access Developer Console https://developer.adobe.com/console and Submit the App for Approval...



16)  Org Administrator can now Review the App and Approve by accessing https://exchange.adobe.com/ > Manage > App Builder applications...



17) App is now Generally Available and loaded on all AEM instances in your Org.. 






No comments:

Post a Comment