Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#7104: affordance to pass data from host page to AA Co-Pilot forms #7109

Merged
merged 10 commits into from
Dec 13, 2023
4 changes: 4 additions & 0 deletions src/background/messenger/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export const waitForTargetByUrl = getMethod("WAIT_FOR_TARGET_BY_URL", bg);
export const activatePartnerTheme = getMethod("ACTIVATE_PARTNER_THEME", bg);
export const getPartnerPrincipals = getMethod("GET_PARTNER_PRINCIPALS", bg);
export const launchAuthIntegration = getMethod("LAUNCH_AUTH_INTEGRATION", bg);
export const setPartnerCopilotData = getNotifier(
"SET_PARTNER_COPILOT_DATA",
bg,
);

export const activateTab = getMethod("ACTIVATE_TAB", bg);
export const reactivateEveryTab = getNotifier("REACTIVATE_EVERY_TAB", bg);
Expand Down
3 changes: 3 additions & 0 deletions src/background/messenger/registration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import {
deleteCachedAuthData,
getCachedAuthData,
} from "@/background/auth/authStorage";
import { setCopilotProcessData } from "@/background/partnerHandlers";

expectContext("background");

Expand All @@ -107,6 +108,7 @@ declare global {
ACTIVATE_PARTNER_THEME: typeof initPartnerTheme;
GET_PARTNER_PRINCIPALS: typeof getPartnerPrincipals;
LAUNCH_AUTH_INTEGRATION: typeof launchAuthIntegration;
SET_PARTNER_COPILOT_DATA: typeof setCopilotProcessData;

INSTALL_STARTER_BLUEPRINTS: typeof installStarterBlueprints;

Expand Down Expand Up @@ -181,6 +183,7 @@ export default function registerMessenger(): void {
ACTIVATE_PARTNER_THEME: initPartnerTheme,
GET_PARTNER_PRINCIPALS: getPartnerPrincipals,
LAUNCH_AUTH_INTEGRATION: launchAuthIntegration,
SET_PARTNER_COPILOT_DATA: setCopilotProcessData,

INSTALL_STARTER_BLUEPRINTS: installStarterBlueprints,

Expand Down
39 changes: 39 additions & 0 deletions src/background/partnerHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { getNotifier, type MessengerMeta } from "webext-messenger";
import {
SET_COPILOT_DATA_MESSAGE_TYPE,
type ProcessDataMap,
} from "@/contrib/automationanywhere/aaFrameProtocol";

type SetCopilotDataRequest = {
/**
* Mapping from process ID to data.
*/
data: ProcessDataMap;
};

/**
* Message the frame parent of the copilot frame to set data on the copilot form.
* @since 1.8.5
* @see initCopilotMessenger
*/
export async function setCopilotProcessData(
this: MessengerMeta,
request: SetCopilotDataRequest,
): Promise<void> {
const sourceTabId = this.trace[0].tab.id;

console.debug("Sending AA Co-Pilot data to frames", {
data: request.data,
});

// Can't use browser.webNavigation.getAllFrames because it doesn't return extension frames
// https://github.com/pixiebrix/pixiebrix-extension/pull/7109#discussion_r1424839790
for (const page of ["/frame.html", "/sidebar.html"]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- receiver not using webext-messenger
const notifier = getNotifier(SET_COPILOT_DATA_MESSAGE_TYPE as any, {
tabId: sourceTabId,
page,
});
notifier(request.data);
}
}
17 changes: 15 additions & 2 deletions src/bricks/renderers/iframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ export class IFrameRenderer extends RendererABC {
type: "string",
description: "The title of the IFrame",
},
// The name field for the iframe. Added in 1.8.5 because the AA copilot frame requires it for messaging.
// @since 1.8.5
name: {
type: "string",
description: "The name of the IFrame",
},
width: {
type: "string",
description: "The width of the IFrame",
Expand All @@ -60,12 +66,14 @@ export class IFrameRenderer extends RendererABC {
async render({
url,
title = "PixieBrix",
name = "",
height = "100%",
width = "100%",
safeMode = false,
}: BrickArgs<{
url: string;
title?: string;
name?: string;
height?: string;
width?: string;
safeMode?: boolean;
Expand All @@ -74,14 +82,19 @@ export class IFrameRenderer extends RendererABC {
const parsedURL = new URL(url);

if (safeMode) {
const namePart = name ? ` name="${name}"` : "";

return assumeSafe(
`<iframe src="${parsedURL.href}" title="${title}" height="${height}" width="${width}" style="border:none;" allowfullscreen="false" allowpaymentrequest="false"></iframe>`,
`<iframe src="${parsedURL.href}" title="${title}" ${namePart} height="${height}" width="${width}" style="border:none;" allowfullscreen="false" allowpaymentrequest="false"></iframe>`,
);
}

// https://transitory.technology/browser-extensions-and-csp-headers/
const frameURL = browser.runtime.getURL("frame.html");
const source = `${frameURL}?url=${encodeURIComponent(parsedURL.href)}`;
const namePart = name ? `&name=${encodeURIComponent(name)}` : "";
const source = `${frameURL}?url=${encodeURIComponent(
parsedURL.href,
)}${namePart}`;

return assumeSafe(
`<iframe src="${source}" title="${title}" height="${height}" width="${width}" style="border:none;"></iframe>`,
Expand Down
2 changes: 2 additions & 0 deletions src/components/fields/fieldUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ const FIELD_TITLE_ACRONYMS = new Set([
"SQL",
"UI",
"URL",
// Discussion: https://english.stackexchange.com/questions/101248/how-should-the-abbreviation-for-identifier-be-capitalized
"ID",
]);

/**
Expand Down
2 changes: 2 additions & 0 deletions src/contentScript/messenger/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ export const runMapArgs = getMethod("RUN_MAP_ARGS");
export const getPageState = getMethod("GET_PAGE_STATE");
export const setPageState = getMethod("SET_PAGE_STATE");

export const getCopilotHostData = getMethod("GET_COPILOT_HOST_DATA");

export const reloadMarketplaceEnhancements = getMethod(
"RELOAD_MARKETPLACE_ENHANCEMENTS",
);
Expand Down
5 changes: 5 additions & 0 deletions src/contentScript/messenger/registration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import { reloadActivationEnhancements } from "@/contentScript/loadActivationEnha
import { getAttributeExamples } from "@/contentScript/pageEditor/elementInformation";
import { closeWalkthroughModal } from "@/contentScript/walkthroughModalProtocol";
import showWalkthroughModal from "@/components/walkthroughModal/showWalkthroughModal";
import { getCopilotHostData } from "@/contrib/automationanywhere/SetCopilotDataEffect";

expectContext("contentScript");

Expand Down Expand Up @@ -144,6 +145,8 @@ declare global {
GET_PAGE_STATE: typeof getPageState;
SET_PAGE_STATE: typeof setPageState;

GET_COPILOT_HOST_DATA: typeof getCopilotHostData;

RELOAD_MARKETPLACE_ENHANCEMENTS: typeof reloadActivationEnhancements;
}
}
Expand Down Expand Up @@ -212,6 +215,8 @@ export default function registerMessenger(): void {
GET_PAGE_STATE: getPageState,
SET_PAGE_STATE: setPageState,

GET_COPILOT_HOST_DATA: getCopilotHostData,

RELOAD_MARKETPLACE_ENHANCEMENTS: reloadActivationEnhancements,
});
}
100 changes: 100 additions & 0 deletions src/contrib/automationanywhere/SetCopilotDataEffect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright (C) 2023 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { propertiesToSchema } from "@/validators/generic";
import { validateRegistryId } from "@/types/helpers";
import { type Schema } from "@/types/schemaTypes";
import { type BrickArgs } from "@/types/runtimeTypes";
import { EffectABC } from "@/types/bricks/effectTypes";
import { type UnknownObject } from "@/types/objectTypes";
import { setPartnerCopilotData } from "@/background/messenger/api";
import { isLoadedInIframe } from "@/utils/iframeUtils";
import { BusinessError } from "@/errors/businessErrors";

type ProcessDataMap = Record<string, UnknownObject>;

// Must track host data on content script instead of the frame parent because the frame parent might not be attached
// to the page at the time this data is set.
const hostData = new Map<string, UnknownObject>();

/**
* Returns the Automation Anywhere Co-Pilot forms data for the current page.
*/
export function getCopilotHostData(): ProcessDataMap {
return Object.fromEntries(hostData.entries());
}

/**
* Brick to map data from the host application to Automation Anywhere Co-Pilot forms.
* https://docs.automationanywhere.com/bundle/enterprise-v2019/page/co-pilot-map-host-data.html
* @since 1.8.5
* @see initCopilotMessenger
*/
export class SetCopilotDataEffect extends EffectABC {
static BRICK_ID = validateRegistryId(
"@pixiebrix/automation-anywhere/set-copilot-data",
);

constructor() {
super(
SetCopilotDataEffect.BRICK_ID,
"Map Automation Anywhere Co-Pilot Data",
"Map host data to Automation Co-Pilot forms",
);
}

inputSchema: Schema = propertiesToSchema(
{
processId: {
title: "Process ID",
type: ["string", "number"],
description: "The Co-Pilot process ID",
},
data: {
title: "Form Data",
type: "object",
additionalProperties: true,
description:
"The form data. See documentation [Map host data to Automation Co-Pilot forms](https://docs.automationanywhere.com/bundle/enterprise-v2019/page/co-pilot-map-host-data.html)",
},
},
["processId", "data"],
);

async effect({
processId,
data,
}: BrickArgs<{
processId: string | number;
data: UnknownObject;
}>): Promise<void> {
if (isLoadedInIframe()) {
// Force the user to use the top-level frame because that's where the copilot protocol will check for data.
throw new BusinessError(
"This brick cannot be used in an iframe. Use target Top-Level Frame.",
);
}

hostData.set(String(processId), data);
twschiller marked this conversation as resolved.
Show resolved Hide resolved

setPartnerCopilotData({
data: Object.fromEntries(hostData.entries()),
});
}
}

export default SetCopilotDataEffect;
Loading
Loading