From cf8fdb9525eb1cad940d6092925bdfcc7dfb85ad Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 16:28:44 -0400 Subject: [PATCH 01/68] getToken/refreshToken --- src/background/auth/getToken.ts | 7 +++++- src/background/refreshToken.ts | 35 +++++++++++++++++++++++------- src/tsconfig.strictNullChecks.json | 2 ++ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/background/auth/getToken.ts b/src/background/auth/getToken.ts index d1affdd8db..0401ad9c23 100644 --- a/src/background/auth/getToken.ts +++ b/src/background/auth/getToken.ts @@ -45,8 +45,13 @@ async function _getToken( throw new Error(`Service ${service.id} does not use token authentication`); } - const { url, data: tokenData } = service.getTokenContext(auth.config); + const context = service.getTokenContext(auth.config); + if (context == null) { + throw new Error("Service did not return a token context"); + } + + const { url, data: tokenData } = context; const { status, statusText, diff --git a/src/background/refreshToken.ts b/src/background/refreshToken.ts index 79fb4d1fbf..9605737009 100644 --- a/src/background/refreshToken.ts +++ b/src/background/refreshToken.ts @@ -39,13 +39,13 @@ import { */ export default async function refreshPKCEToken( integration: Integration, - integrationConfig: SanitizedIntegrationConfig, + sanitizedConfig: SanitizedIntegrationConfig, ): Promise { expectContext("background"); - if (integration.id !== integrationConfig.serviceId) { + if (integration.id !== sanitizedConfig.serviceId) { throw new Error( - `Integration id and config service id do not match: ${integration.id} !== ${integrationConfig.serviceId}`, + `Integration id and config service id do not match: ${integration.id} !== ${sanitizedConfig.serviceId}`, ); } else if (integration.id === CONTROL_ROOM_OAUTH_INTEGRATION_ID) { throw new Error( @@ -57,22 +57,41 @@ export default async function refreshPKCEToken( ); } - const cachedAuthData = await getCachedAuthData(integrationConfig.id); + const cachedAuthData = await getCachedAuthData(sanitizedConfig.id); // The PIXIEBRIX_INTEGRATION_ID check is mostly for backwards compatibility - // to avoid dealing with undefined integrationConfig id's. + // to avoid dealing with undefined sanitizedConfig id's. if ( integration.id !== PIXIEBRIX_INTEGRATION_ID && cachedAuthData?.refresh_token ) { console.debug("Refreshing PKCE token"); - const { config } = await serviceLocator.findIntegrationConfig( - integrationConfig.id, + const integrationConfig = await serviceLocator.findIntegrationConfig( + sanitizedConfig.id, ); - const { tokenUrl, client_id, client_secret } = + if (integrationConfig == null) { + throw new Error( + `Integration config not found for config id ${sanitizedConfig.id}`, + ); + } + + const { config } = integrationConfig; + const oauth2Context = // XXX: pass interactive: true to match the legacy behavior integration.getOAuth2Context(config, { interactive: true }); + if (oauth2Context == null) { + throw new Error( + `OAuth2 context not found in config for ${integration.id}`, + ); + } + + const { tokenUrl, client_id, client_secret } = oauth2Context; + if (tokenUrl == null) { + throw new Error( + `OAuth2 PKCE token URL not found in OAuth2 context for ${integration.id}`, + ); + } // https://axios-http.com/docs/urlencoded const params = new URLSearchParams(); diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index 2bbccb858b..333f7c6ebf 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -4,6 +4,8 @@ "strictNullChecks": true }, "files": [ + "./background/refreshToken.ts", + "./background/auth/getToken.ts", "./bricks/transformers/controlFlow/Retry.ts", "./bricks/effects/AddQuickBarAction.tsx", "./background/browserAction.ts", From c48b8f34f49e1ba0a448f55e06fa46a491215132 Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 16:29:05 -0400 Subject: [PATCH 02/68] src/background/requests.ts --- src/background/requests.ts | 12 ++---------- src/tsconfig.strictNullChecks.json | 1 + 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/background/requests.ts b/src/background/requests.ts index f883b37c62..b02f75329f 100644 --- a/src/background/requests.ts +++ b/src/background/requests.ts @@ -76,13 +76,7 @@ type SanitizedResponse = Pick< const UNAUTHORIZED_STATUS_CODES = new Set([401, 403]); -function sanitizeResponse( - response: AxiosResponse | null, -): SanitizedResponse | null { - if (response == null) { - return null; - } - +function sanitizeResponse(response: AxiosResponse): SanitizedResponse { const { data, status, statusText } = response; return ensureJsonObject({ data, status, statusText }) as SanitizedResponse; } @@ -124,9 +118,7 @@ async function serializableAxiosRequest( // Axios does not perform validation, so call before axios assertProtocolUrl(config.url, ["https:", "http:"]); - const response = await axios(config); - - return sanitizeResponse(response); + return sanitizeResponse(await axios(config)); } /** diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index 333f7c6ebf..38cdbbc6e3 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -4,6 +4,7 @@ "strictNullChecks": true }, "files": [ + "./background/requests.ts", "./background/refreshToken.ts", "./background/auth/getToken.ts", "./bricks/transformers/controlFlow/Retry.ts", From 619e8beb5840e6b3a9438f194a2f605d5eb3d280 Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 16:47:52 -0400 Subject: [PATCH 03/68] src/contentScript/pageEditor/elementPicker.ts --- src/contentScript/pageEditor/elementPicker.ts | 73 ++++++++++--------- src/tsconfig.strictNullChecks.json | 1 + src/vendors/Overlay.tsx | 8 +- 3 files changed, 45 insertions(+), 37 deletions(-) diff --git a/src/contentScript/pageEditor/elementPicker.ts b/src/contentScript/pageEditor/elementPicker.ts index 08d4e9944d..57d2168f3b 100644 --- a/src/contentScript/pageEditor/elementPicker.ts +++ b/src/contentScript/pageEditor/elementPicker.ts @@ -38,12 +38,12 @@ let expandOverlay: Overlay | null = null; * Overlay for the root element. */ let rootOverlay: Overlay | null = null; -let styleElement: HTMLStyleElement = null; -let multiSelectionToolElement: HTMLElement = null; -let selectionHandler: SelectionHandlerType; -let stopInspectingNative: () => void; +let styleElement: HTMLStyleElement | null = null; +let multiSelectionToolElement: HTMLElement | null = null; +let selectionHandler: SelectionHandlerType | null = null; +let stopInspectingNative: (() => void) | null = null; -function setSelectionHandler(handler: SelectionHandlerType) { +function setSelectionHandler(handler: SelectionHandlerType): void { selectionHandler = handler; } @@ -60,7 +60,7 @@ export function stopInspectingNativeHandler(): void { stopInspectingNative?.(); } -let _cancelSelect: () => void = null; +let _cancelSelect: (() => void) | null = null; interface UserSelection { /** Element(s) to limit the selection to. */ @@ -108,8 +108,8 @@ export async function userSelectElement({ function highlightRoots() { // Highlight and scroll to root element so the user knows where they can click if (roots.length > 0) { - roots[0].scrollTo({ behavior: "smooth" }); - rootOverlay.inspect(roots); + roots[0]?.scrollTo({ behavior: "smooth" }); + rootOverlay?.inspect(roots); } } @@ -123,7 +123,7 @@ export async function userSelectElement({ return; } - overlay.inspect(filteredElements); + overlay?.inspect(filteredElements); setTimeout(() => requestAnimationFrame(updateOverlay), 30); // Only when the tab is visible }; @@ -133,9 +133,9 @@ export async function userSelectElement({ } } - function findExpectedTarget(target: EventTarget): HTMLElement | void { + function findExpectedTarget(target: EventTarget): HTMLElement | null { if (!(target instanceof HTMLElement)) { - return; + return null; } if (!filter) { @@ -145,7 +145,7 @@ export async function userSelectElement({ return target.closest(filter); } - function startInspectingNative() { + function startInspectingNative(): void { _cancelSelect = cancel; registerListenersOnWindow(window); addInspectingModeStyles(window); @@ -157,7 +157,7 @@ export async function userSelectElement({ onContextInvalidated.addListener(cancel); } - function handleDone(target?: HTMLElement) { + function handleDone(target?: HTMLElement): void { try { const result = uniq(compact([...targets, target])); if ( @@ -181,28 +181,29 @@ export async function userSelectElement({ isMulti = value; if (!isMulti) { shouldSelectSimilar = false; - overlay.inspect([]); - expandOverlay.inspect([]); + overlay?.inspect([]); + expandOverlay?.inspect([]); targets.clear(); - selectionHandler(targets.size); + selectionHandler?.(targets.size); } } function handleSimilarSelectionChange(value: boolean) { shouldSelectSimilar = value; if (shouldSelectSimilar) { - const commonSelector = expandedCssSelector([...targets]); + const commonSelector = expandedCssSelector([...targets]) ?? ""; const expandTargets = difference($(commonSelector), [...targets]); - selectionHandler(expandTargets.length); - expandOverlay.inspect([...expandTargets]); + selectionHandler?.(expandTargets.length); + expandOverlay?.inspect([...expandTargets]); } else { - selectionHandler(targets.size); - expandOverlay.inspect([]); + selectionHandler?.(targets.size); + expandOverlay?.inspect([]); } } function noopMouseHandler(event: MouseEvent) { - const target = findExpectedTarget(event.target); + const target: HTMLElement | null = + event.target == null ? null : findExpectedTarget(event.target); if (!target) { event.preventDefault(); event.stopPropagation(); @@ -219,7 +220,8 @@ export async function userSelectElement({ } function onClick(event: MouseEvent) { - const target = findExpectedTarget(event.target); + const target: HTMLElement | null = + event.target == null ? null : findExpectedTarget(event.target); if (event.altKey || !target) { return; } @@ -239,16 +241,16 @@ export async function userSelectElement({ targets.add(target); } - overlay.inspect([...targets]); + overlay?.inspect([...targets]); if (targets.size > 1 && shouldSelectSimilar) { - const commonSelector = expandedCssSelector([...targets]); + const commonSelector = expandedCssSelector([...targets]) ?? ""; const expandTargets = difference($(commonSelector), [...targets]); - selectionHandler(expandTargets.length); - expandOverlay.inspect([...expandTargets]); + selectionHandler?.(expandTargets.length); + expandOverlay?.inspect([...expandTargets]); } else { - selectionHandler(targets.size); - expandOverlay.inspect([]); + selectionHandler?.(targets.size); + expandOverlay?.inspect([]); } return; @@ -258,6 +260,10 @@ export async function userSelectElement({ } function onPointerDown(event: MouseEvent) { + if (event.target == null) { + return; + } + const target = findExpectedTarget(event.target); if (!target) { event.preventDefault(); @@ -279,15 +285,16 @@ export async function userSelectElement({ function onPointerOver(event: MouseEvent) { event.preventDefault(); event.stopPropagation(); - const target = findExpectedTarget(event.target); + const target: HTMLElement | null = + event.target == null ? null : findExpectedTarget(event.target); if (target) { - overlay.inspect([...targets, target]); + overlay?.inspect([...targets, target]); } } function onPointerLeave() { - overlay.inspect([...targets]); + overlay?.inspect([...targets]); } function escape(event: KeyboardEvent) { @@ -299,7 +306,7 @@ export async function userSelectElement({ } function cancel() { - stopInspectingNative(); + stopInspectingNative?.(); reject(new CancelError("Selection cancelled")); } diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index 38cdbbc6e3..acff4a656d 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -4,6 +4,7 @@ "strictNullChecks": true }, "files": [ + "./contentScript/pageEditor/elementPicker.ts", "./background/requests.ts", "./background/refreshToken.ts", "./background/auth/getToken.ts", diff --git a/src/vendors/Overlay.tsx b/src/vendors/Overlay.tsx index 8f50d5eda4..4115a57618 100644 --- a/src/vendors/Overlay.tsx +++ b/src/vendors/Overlay.tsx @@ -281,7 +281,7 @@ export default class Overlay { while (this.rects.length > elements.length) { const rect = this.rects.pop(); - rect.remove(); + rect?.remove(); } if (elements.length === 0) { return; @@ -314,11 +314,11 @@ export default class Overlay { outerBox.left = Math.min(outerBox.left, box.left - dims.marginLeft); const rect = this.rects[index]; - rect.update(box, dims); + rect?.update(box, dims); } if (!name) { - name = elements[0].nodeName.toLowerCase(); + name = elements[0]!.nodeName.toLowerCase(); } this.tip.updateText( @@ -397,7 +397,7 @@ function getNestedBoundingClientRect( const ownerIframe = getOwnerIframe(node); if (ownerIframe) { const rects = [node.getBoundingClientRect()]; - let currentIframe = ownerIframe; + let currentIframe: HTMLElement | null = ownerIframe; let onlyOneMore = false; while (currentIframe) { const rect = getBoundingClientRectWithBorderOffset(currentIframe); From 96786b02acd439dd46e5dbcb00ba47dd398c94ba Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 17:19:25 -0400 Subject: [PATCH 04/68] src/background/partnerIntegrations.ts --- src/background/auth/getToken.ts | 6 +- src/background/partnerIntegrations.ts | 92 +++++++++++++++++++-------- src/background/requests.ts | 10 +-- src/tsconfig.strictNullChecks.json | 1 + 4 files changed, 69 insertions(+), 40 deletions(-) diff --git a/src/background/auth/getToken.ts b/src/background/auth/getToken.ts index 0401ad9c23..16f5e37558 100644 --- a/src/background/auth/getToken.ts +++ b/src/background/auth/getToken.ts @@ -24,6 +24,7 @@ import { expectContext } from "@/utils/expectContext"; import axios from "axios"; import { setCachedAuthData } from "@/background/auth/authStorage"; import { memoizeUntilSettled } from "@/utils/promiseUtils"; +import { assertNotNullish } from "@/utils/nullishUtils"; /** * Exchange credentials for a token, and cache the token response. @@ -46,10 +47,7 @@ async function _getToken( } const context = service.getTokenContext(auth.config); - - if (context == null) { - throw new Error("Service did not return a token context"); - } + assertNotNullish(context, "Service did not return a token context"); const { url, data: tokenData } = context; const { diff --git a/src/background/partnerIntegrations.ts b/src/background/partnerIntegrations.ts index 0c821b1883..3f46a14574 100644 --- a/src/background/partnerIntegrations.ts +++ b/src/background/partnerIntegrations.ts @@ -16,7 +16,7 @@ */ import { locator as serviceLocator } from "@/background/locator"; -import { flatten, isEmpty } from "lodash"; +import { compact, flatten } from "lodash"; import { expectContext } from "@/utils/expectContext"; import { type RegistryId } from "@/types/registryTypes"; import launchOAuth2Flow from "@/background/auth/launchOAuth2Flow"; @@ -34,6 +34,7 @@ import { } from "@/integrations/constants"; import { stringToBase64 } from "uint8array-extras"; import { canParseUrl } from "@/utils/urlUtils"; +import { assertNotNullish } from "@/utils/nullishUtils"; const TEN_HOURS = 1000 * 60 * 60 * 10; @@ -77,12 +78,18 @@ export async function getPartnerPrincipals(): Promise { ), ); - return auths - .filter((auth) => canParseUrl(auth.config.controlRoomUrl)) - .map((auth) => ({ - hostname: new URL(auth.config.controlRoomUrl).hostname, - principalId: auth.config.username, - })); + return compact( + auths.map((auth) => { + if (canParseUrl(auth.config.controlRoomUrl)) { + return { + hostname: new URL(auth.config.controlRoomUrl).hostname, + principalId: null, + } as PartnerPrincipal; + } + + return null; + }), + ); } /** @@ -112,17 +119,31 @@ export async function launchAuthIntegration({ console.warn("Multiple local configurations found for: %s", service.id); } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion,@typescript-eslint/no-unnecessary-type-assertion -- just checked array length + const authId = localAuths[0]!.id; + // `launchOAuth2Flow` expects the raw auth. In the case of CONTROL_ROOM_OAUTH_SERVICE_ID, they'll be the same // because it doesn't have any secrets. - const config = await serviceLocator.findIntegrationConfig(localAuths[0].id); - const data = await launchOAuth2Flow(service, config, { interactive: true }); + const integrationConfig = await serviceLocator.findIntegrationConfig(authId); + assertNotNullish( + integrationConfig, + `Integration config not found for authId: ${authId}`, + ); + + const data = await launchOAuth2Flow(service, integrationConfig, { + interactive: true, + }); if (integrationId === CONTROL_ROOM_OAUTH_INTEGRATION_ID) { // Hard-coding headers for now. In the future, will want to add support for defining in the service definition. - if (isEmpty(config.config.controlRoomUrl)) { + const { controlRoomUrl } = integrationConfig.config; + if (!canParseUrl(controlRoomUrl)) { // Fine to dump to console for debugging because CONTROL_ROOM_OAUTH_SERVICE_ID doesn't have any secret props. - console.warn("controlRoomUrl is missing on configuration", config); + console.warn( + "controlRoomUrl is missing on configuration", + integrationConfig, + ); throw new Error("controlRoomUrl is missing on configuration"); } @@ -137,11 +158,15 @@ export async function launchAuthIntegration({ baseURL, headers: { Authorization: `Bearer ${data.access_token as string}`, - "X-Control-Room": config.config.controlRoomUrl, + "X-Control-Room": controlRoomUrl, }, }); } catch (error) { - if (isAxiosError(error) && [401, 403].includes(error.response?.status)) { + if ( + isAxiosError(error) && + error.response && + [401, 403].includes(error.response.status) + ) { // Clear the token to allow the user re-login with the SAML/SSO provider // https://developer.chrome.com/docs/extensions/reference/identity/#method-clearAllCachedAuthTokens await chromeP.identity.clearAllCachedAuthTokens(); @@ -155,18 +180,15 @@ export async function launchAuthIntegration({ throw error; } - console.info( - "Setting partner auth for Control Room %s", - config.config.controlRoomUrl, - ); + console.info("Setting partner auth for Control Room %s", controlRoomUrl); await setPartnerAuth({ - authId: config.id, + authId: integrationConfig.id, token: data.access_token as string, // `refresh_token` only returned if offline_access scope is requested refreshToken: data.refresh_token as string, extraHeaders: { - "X-Control-Room": config.config.controlRoomUrl, + "X-Control-Room": controlRoomUrl, }, }); } else { @@ -190,21 +212,37 @@ export async function _refreshPartnerToken(): Promise { const service = await serviceRegistry.lookup( CONTROL_ROOM_OAUTH_INTEGRATION_ID, ); - const config = await serviceLocator.findIntegrationConfig(authData.authId); - const context = service.getOAuth2Context(config.config); + const integrationConfig = await serviceLocator.findIntegrationConfig( + authData.authId, + ); + assertNotNullish( + integrationConfig, + `Integration config not found for authId: ${authData.authId}`, + ); - if (isEmpty(config.config.controlRoomUrl)) { + const { controlRoomUrl } = integrationConfig.config; + if (!canParseUrl(controlRoomUrl)) { // Fine to dump to console for debugging because CONTROL_ROOM_OAUTH_SERVICE_ID doesn't have any secret props. - console.warn("controlRoomUrl is missing on configuration", config); + console.warn( + "controlRoomUrl is missing on configuration", + integrationConfig, + ); throw new Error("controlRoomUrl is missing on configuration"); } + const context = service.getOAuth2Context(integrationConfig.config); + assertNotNullish(context, "Service did not return an OAuth2 context"); + assertNotNullish( + context.tokenUrl, + `OAuth2 context for service ${integrationConfig.integrationId} does not include a token URL`, + ); + // https://axios-http.com/docs/urlencoded const params = new URLSearchParams(); params.append("grant_type", "refresh_token"); params.append("client_id", context.client_id); params.append("refresh_token", authData.refreshToken); - params.append("hosturl", config.config.controlRoomUrl); + params.append("hosturl", controlRoomUrl); // On 401, throw the error. In the future, we might consider clearing the partnerAuth. However, currently that // would trigger a re-login, which may not be desirable at arbitrary times. @@ -213,16 +251,16 @@ export async function _refreshPartnerToken(): Promise { }); // Store for use direct calls to the partner API - await setCachedAuthData(config.id, data); + await setCachedAuthData(integrationConfig.id, data); // Store for use with the PixieBrix API await setPartnerAuth({ - authId: config.id, + authId: integrationConfig.id, token: data.access_token, // `refresh_token` only returned if offline_access scope is requested refreshToken: data.refresh_token, extraHeaders: { - "X-Control-Room": config.config.controlRoomUrl, + "X-Control-Room": controlRoomUrl, }, }); diff --git a/src/background/requests.ts b/src/background/requests.ts index b02f75329f..3a8748e770 100644 --- a/src/background/requests.ts +++ b/src/background/requests.ts @@ -26,9 +26,9 @@ import { expectContext } from "@/utils/expectContext"; import { absoluteApiUrl } from "@/data/service/apiClient"; import { type ProxyResponseData, type RemoteResponse } from "@/types/contract"; import { - selectRemoteResponseErrorMessage, isProxiedErrorResponse, proxyResponseToAxiosResponse, + selectRemoteResponseErrorMessage, } from "@/background/proxyUtils"; import { selectAxiosError } from "@/data/service/requestErrorUtils"; import { @@ -151,10 +151,6 @@ async function authenticate( ): Promise { expectContext("background"); - if (config == null) { - throw new Error("Integration configuration is required to authenticate"); - } - if (config.proxy) { throw new Error( `Integration configuration for service ${config.serviceId} is not a local configuration: ${config.id}`, @@ -219,10 +215,6 @@ async function proxyRequest( integrationConfig: SanitizedIntegrationConfig, requestConfig: NetworkRequestConfig, ): Promise> { - if (integrationConfig == null) { - throw new Error("Integration configuration is required for proxyRequest"); - } - const authenticatedRequestConfig = await authenticate( pixiebrixConfigurationFactory(), { diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index acff4a656d..cc7b03ea7f 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -4,6 +4,7 @@ "strictNullChecks": true }, "files": [ + "./background/partnerIntegrations.ts", "./contentScript/pageEditor/elementPicker.ts", "./background/requests.ts", "./background/refreshToken.ts", From 19c60ed6c5010423f30d6a14644d72ee1a98f2aa Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 18:58:23 -0400 Subject: [PATCH 05/68] whoops --- src/background/partnerIntegrations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/background/partnerIntegrations.ts b/src/background/partnerIntegrations.ts index 3f46a14574..8200720f92 100644 --- a/src/background/partnerIntegrations.ts +++ b/src/background/partnerIntegrations.ts @@ -83,7 +83,7 @@ export async function getPartnerPrincipals(): Promise { if (canParseUrl(auth.config.controlRoomUrl)) { return { hostname: new URL(auth.config.controlRoomUrl).hostname, - principalId: null, + principalId: auth.config.username, } as PartnerPrincipal; } From fa36bd629b8dc1b66a4414ddd9c5211e15bd8564 Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 18:59:07 -0400 Subject: [PATCH 06/68] cleanup --- src/background/partnerIntegrations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/background/partnerIntegrations.ts b/src/background/partnerIntegrations.ts index 8200720f92..0d4164506e 100644 --- a/src/background/partnerIntegrations.ts +++ b/src/background/partnerIntegrations.ts @@ -83,7 +83,7 @@ export async function getPartnerPrincipals(): Promise { if (canParseUrl(auth.config.controlRoomUrl)) { return { hostname: new URL(auth.config.controlRoomUrl).hostname, - principalId: auth.config.username, + principalId: auth.config.username ?? null, } as PartnerPrincipal; } From 0cdffaf7d46fd78c5754e931202753788e533796 Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 18:40:18 -0400 Subject: [PATCH 07/68] fix widget and getScopeAndId --- .../form/widgets/RegistryIdWidget.tsx | 32 +++++++++++++------ src/components/form/widgets/SelectWidget.tsx | 6 ---- .../{OwnerLabel.tsx => ModOwnerLabel.tsx} | 17 ++++------ .../shareModals/PublishContentLayout.tsx | 4 +-- .../shareModals/ShareRecipeModalBody.tsx | 6 ++-- .../shareModals/useHasEditPermissions.ts | 12 +++---- src/hooks/useMemoCompare.ts | 28 ++++++++++++++-- src/modDefinitions/modDefinitionHooks.ts | 10 +++--- src/tsconfig.strictNullChecks.json | 4 +++ src/utils/registryUtils.ts | 19 ++++------- 10 files changed, 80 insertions(+), 58 deletions(-) rename src/extensionConsole/pages/mods/modals/shareModals/{OwnerLabel.tsx => ModOwnerLabel.tsx} (81%) diff --git a/src/components/form/widgets/RegistryIdWidget.tsx b/src/components/form/widgets/RegistryIdWidget.tsx index d29664ed10..6933ab8690 100644 --- a/src/components/form/widgets/RegistryIdWidget.tsx +++ b/src/components/form/widgets/RegistryIdWidget.tsx @@ -20,10 +20,10 @@ import React from "react"; import { useSelector } from "react-redux"; import { selectAuth } from "@/auth/authSelectors"; import SelectWidget, { - makeStringOptions, + type Option, type SelectWidgetOnChange, } from "@/components/form/widgets/SelectWidget"; -import { isEmpty } from "lodash"; +import { compact } from "lodash"; import { type RegistryId } from "@/types/registryTypes"; // eslint-disable-next-line no-restricted-imports -- TODO: Fix over time import { Form } from "react-bootstrap"; @@ -33,6 +33,7 @@ import { UserRole } from "@/types/contract"; import { getScopeAndId } from "@/utils/registryUtils"; import useAsyncEffect from "use-async-effect"; +import { assertNotNullish } from "@/utils/nullishUtils"; const editorRoles = new Set([ UserRole.admin, @@ -48,16 +49,27 @@ const RegistryIdWidget: React.VFC<{ }> = ({ name, selectStyles = emptyObject }) => { const [{ value }, , { setValue }] = useField(name); const { scope: userScope, organizations } = useSelector(selectAuth); - const organizationScopes = organizations - .filter( - (organization) => - !isEmpty(organization.scope) && editorRoles.has(organization.role), - ) - .map((organization) => organization.scope); + // XXX: We should eventually refactor RequireScope to pass the required (non-null) user scope down to children, or set a context value + assertNotNullish( + userScope, + "This widget should only be used inside RequireScope, userScope should be defined", + ); + const organizationScopes: string[] = compact( + organizations.map(({ scope, role }) => { + if (scope && editorRoles.has(role)) { + return scope; + } - const options = makeStringOptions(userScope, ...organizationScopes); + return null; + }), + ); + const scopes: string[] = [userScope, ...organizationScopes]; + const options: Option[] = scopes.map((scope) => ({ + label: scope, + value: scope, + })); - const [scopeValue = userScope, idValue] = getScopeAndId(value); + const { scope: scopeValue = userScope, id: idValue } = getScopeAndId(value); useAsyncEffect(async () => { // Don't validate here with validateRegistryId(), that should be done through form validation diff --git a/src/components/form/widgets/SelectWidget.tsx b/src/components/form/widgets/SelectWidget.tsx index 65b398513e..1e510d488d 100644 --- a/src/components/form/widgets/SelectWidget.tsx +++ b/src/components/form/widgets/SelectWidget.tsx @@ -66,12 +66,6 @@ export type SelectWidgetProps> = creatable?: boolean; }; -export const makeStringOptions = (...items: string[]): Option[] => - items.map((item) => ({ - label: item, - value: item, - })); - const SelectWidget = >({ id, options, diff --git a/src/extensionConsole/pages/mods/modals/shareModals/OwnerLabel.tsx b/src/extensionConsole/pages/mods/modals/shareModals/ModOwnerLabel.tsx similarity index 81% rename from src/extensionConsole/pages/mods/modals/shareModals/OwnerLabel.tsx rename to src/extensionConsole/pages/mods/modals/shareModals/ModOwnerLabel.tsx index e3a72a694d..5c70defc0b 100644 --- a/src/extensionConsole/pages/mods/modals/shareModals/OwnerLabel.tsx +++ b/src/extensionConsole/pages/mods/modals/shareModals/ModOwnerLabel.tsx @@ -17,7 +17,6 @@ import { selectAuth } from "@/auth/authSelectors"; import useSortOrganizations from "@/extensionConsole/pages/mods/modals/shareModals/useSortOrganizations"; -import { useOptionalModDefinition } from "@/modDefinitions/modDefinitionHooks"; import { type RegistryId } from "@/types/registryTypes"; import { getScopeAndId } from "@/utils/registryUtils"; import { faUser, faUsers } from "@fortawesome/free-solid-svg-icons"; @@ -25,18 +24,14 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import React from "react"; import { useSelector } from "react-redux"; -const OwnerLabel: React.FunctionComponent<{ blueprintId: RegistryId }> = ({ - blueprintId, +const ModOwnerLabel: React.FunctionComponent<{ modId: RegistryId }> = ({ + modId, }) => { const { scope: userScope } = useSelector(selectAuth); - - const { data: recipe } = useOptionalModDefinition(blueprintId); - + const { scope: modIdScope } = getScopeAndId(modId); const sortedOrganizations = useSortOrganizations(); - const [recipeScope] = getScopeAndId(recipe?.metadata.id); - - if (recipeScope === userScope) { + if (modIdScope === userScope) { return ( You @@ -45,7 +40,7 @@ const OwnerLabel: React.FunctionComponent<{ blueprintId: RegistryId }> = ({ } const ownerOrganization = sortedOrganizations.find( - (x) => x.scope === recipeScope, + (x) => x.scope === modIdScope, ); if (!ownerOrganization) { @@ -63,4 +58,4 @@ const OwnerLabel: React.FunctionComponent<{ blueprintId: RegistryId }> = ({ ); }; -export default OwnerLabel; +export default ModOwnerLabel; diff --git a/src/extensionConsole/pages/mods/modals/shareModals/PublishContentLayout.tsx b/src/extensionConsole/pages/mods/modals/shareModals/PublishContentLayout.tsx index ea7b1a442b..41f20bd1bc 100644 --- a/src/extensionConsole/pages/mods/modals/shareModals/PublishContentLayout.tsx +++ b/src/extensionConsole/pages/mods/modals/shareModals/PublishContentLayout.tsx @@ -24,7 +24,7 @@ import { faInfoCircle, faUsers } from "@fortawesome/free-solid-svg-icons"; import styles from "./ShareModals.module.scss"; import { useOptionalModDefinition } from "@/modDefinitions/modDefinitionHooks"; import { RequireScope } from "@/auth/RequireScope"; -import OwnerLabel from "@/extensionConsole/pages/mods/modals/shareModals/OwnerLabel"; +import ModOwnerLabel from "@/extensionConsole/pages/mods/modals/shareModals/ModOwnerLabel"; import useHasEditPermissions from "@/extensionConsole/pages/mods/modals/shareModals/useHasEditPermissions"; import useSortOrganizations from "@/extensionConsole/pages/mods/modals/shareModals/useSortOrganizations"; @@ -50,7 +50,7 @@ const PublishContentLayout: React.FunctionComponent< to change sharing
- + Owner
{sortedOrganizations diff --git a/src/extensionConsole/pages/mods/modals/shareModals/ShareRecipeModalBody.tsx b/src/extensionConsole/pages/mods/modals/shareModals/ShareRecipeModalBody.tsx index 7a02e9e229..b4afa8a2aa 100644 --- a/src/extensionConsole/pages/mods/modals/shareModals/ShareRecipeModalBody.tsx +++ b/src/extensionConsole/pages/mods/modals/shareModals/ShareRecipeModalBody.tsx @@ -46,7 +46,7 @@ import createMenuListWithAddButton from "@/components/form/widgets/createMenuLis import { type Option } from "@/components/form/widgets/SelectWidget"; import Loader from "@/components/Loader"; import useHasEditPermissions from "@/extensionConsole/pages/mods/modals/shareModals/useHasEditPermissions"; -import OwnerLabel from "@/extensionConsole/pages/mods/modals/shareModals/OwnerLabel"; +import ModOwnerLabel from "@/extensionConsole/pages/mods/modals/shareModals/ModOwnerLabel"; import useSortOrganizations from "@/extensionConsole/pages/mods/modals/shareModals/useSortOrganizations"; type ShareModFormState = { @@ -172,7 +172,7 @@ const ShareRecipeModalBody: React.FunctionComponent = () => { />
- + Owner
@@ -224,7 +224,7 @@ const ShareRecipeModalBody: React.FunctionComponent = () => { permissions to change sharing
- + Owner
{organizationsForSelect diff --git a/src/extensionConsole/pages/mods/modals/shareModals/useHasEditPermissions.ts b/src/extensionConsole/pages/mods/modals/shareModals/useHasEditPermissions.ts index ebac0c22a0..d774b4bfd5 100644 --- a/src/extensionConsole/pages/mods/modals/shareModals/useHasEditPermissions.ts +++ b/src/extensionConsole/pages/mods/modals/shareModals/useHasEditPermissions.ts @@ -17,7 +17,6 @@ import { selectAuth } from "@/auth/authSelectors"; import useSortOrganizations from "@/extensionConsole/pages/mods/modals/shareModals/useSortOrganizations"; -import { useOptionalModDefinition } from "@/modDefinitions/modDefinitionHooks"; import { UserRole } from "@/types/contract"; import { type RegistryId } from "@/types/registryTypes"; import { getScopeAndId } from "@/utils/registryUtils"; @@ -25,21 +24,18 @@ import { useSelector } from "react-redux"; const editorRoles = new Set([UserRole.admin, UserRole.developer]); -export default function useHasEditPermissions(blueprintId: RegistryId) { +export default function useHasEditPermissions(modId: RegistryId) { const { scope: userScope } = useSelector(selectAuth); - - const { data: recipe } = useOptionalModDefinition(blueprintId); - + const { scope: modIdScope } = getScopeAndId(modId); const sortedOrganizations = useSortOrganizations(); - const [recipeScope] = getScopeAndId(recipe?.metadata.id); let hasEditPermissions = false; - if (recipeScope === userScope) { + if (modIdScope === userScope) { hasEditPermissions = true; } else { const ownerOrganization = sortedOrganizations.find( - (x) => x.scope === recipeScope, + (x) => x.scope === modIdScope, ); if (ownerOrganization) { diff --git a/src/hooks/useMemoCompare.ts b/src/hooks/useMemoCompare.ts index 79967560be..a82718fce2 100644 --- a/src/hooks/useMemoCompare.ts +++ b/src/hooks/useMemoCompare.ts @@ -16,15 +16,18 @@ */ import { useEffect, useRef } from "react"; +import deepEquals from "fast-deep-equal"; /** * Utility hook to return a referentially stable value if the next value is equal to the previous value. * @param next the next value * @param compare comparison function to determine whether to return the previous value + * @param dependencies extra memoization dependencies outside the returned value */ function useMemoCompare( next: T, compare: (previous: T | undefined, next: T) => boolean, + dependencies?: unknown[], ): T { // https://usehooks.com/useMemoCompare/ // Ref for storing previous value @@ -40,9 +43,30 @@ function useMemoCompare( } }); - // Finally, if equal then return the previous value + const previousDependenciesRef = useRef(); + const previousDependencies: unknown[] | undefined = + previousDependenciesRef.current; + + let isDependenciesEqual: boolean; + if (dependencies === undefined && previousDependencies === undefined) { + isDependenciesEqual = true; + } + + if (dependencies === undefined || previousDependencies === undefined) { + isDependenciesEqual = false; + } else { + isDependenciesEqual = deepEquals(previousDependencies, dependencies); + } + + useEffect(() => { + if (!isDependenciesEqual) { + previousDependenciesRef.current = dependencies; + } + }); + + // Finally, if equal, and dependencies have not changed, then return the previous value // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion -- Can't be undefined if it's equal to T - return isEqual ? previous! : next; + return isEqual && isDependenciesEqual ? previous! : next; } export default useMemoCompare; diff --git a/src/modDefinitions/modDefinitionHooks.ts b/src/modDefinitions/modDefinitionHooks.ts index e450d1967d..7aacd19bf6 100644 --- a/src/modDefinitions/modDefinitionHooks.ts +++ b/src/modDefinitions/modDefinitionHooks.ts @@ -58,13 +58,14 @@ export function useOptionalModDefinition( const modDefinitionState = useMergeAsyncState(state, findModDefinition); // Avoid reference change when useAllModDefinitions switches from cache to remote fetch - const data = useMemoCompare( + const data = useMemoCompare( modDefinitionState.data, deepEquals, ); - const currentData = useMemoCompare( + const currentData = useMemoCompare( modDefinitionState.data, deepEquals, + [id], ); return { @@ -111,13 +112,14 @@ export function useRequiredModDefinitions( ); // Avoid reference change when useAllModDefinitions switches from cache to remote fetch - const data = useMemoCompare( + const data = useMemoCompare( modDefinitionState.data, deepEquals, ); - const currentData = useMemoCompare( + const currentData = useMemoCompare( modDefinitionState.currentData, deepEquals, + ids, ); // Don't error until the lookup fails against the remote data diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index cc7b03ea7f..fb602afe41 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -4,6 +4,10 @@ "strictNullChecks": true }, "files": [ + "./extensionConsole/pages/mods/modals/shareModals/ModOwnerLabel.tsx", + "./modDefinitions/modDefinitionHooks.ts", + "./extensionConsole/pages/mods/modals/shareModals/useHasEditPermissions.ts", + "./components/form/widgets/RegistryIdWidget.tsx", "./background/partnerIntegrations.ts", "./contentScript/pageEditor/elementPicker.ts", "./background/requests.ts", diff --git a/src/utils/registryUtils.ts b/src/utils/registryUtils.ts index b38b4d6e88..7699031ba8 100644 --- a/src/utils/registryUtils.ts +++ b/src/utils/registryUtils.ts @@ -40,25 +40,20 @@ export function generatePackageId(scope: string, label: string): RegistryId { * as everything following the first / character * @param value the full RegistryId */ -export function getScopeAndId( - value: RegistryId | null, -): [string | undefined, string | undefined] { - // We call getScopeAndId in several places with a recipe that can be undefined - // @see useHasEditPermissions.ts - if (value == null) { - return [undefined, undefined]; - } - +export function getScopeAndId(value: RegistryId): { + scope: string | undefined; + id: string | undefined; +} { // Scope needs to start with @ if (!value.startsWith("@")) { - return [undefined, value]; + return { scope: undefined, id: value }; } // If the value starts with @ and doesn't have a slash, interpret it as a scope if (!value.includes("/")) { - return [value, undefined]; + return { scope: value, id: undefined }; } const [scope, ...idParts] = split(value, "/"); - return [scope, idParts.join("/")]; + return { scope, id: idParts.join("/") }; } From 6d37d22f0141955c72224c5dfa1e7c5d2a12c84c Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 19:05:56 -0400 Subject: [PATCH 08/68] auto-add --- src/tsconfig.strictNullChecks.json | 79 +++++++++++++++--------------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index fb602afe41..4db67db701 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -4,44 +4,6 @@ "strictNullChecks": true }, "files": [ - "./extensionConsole/pages/mods/modals/shareModals/ModOwnerLabel.tsx", - "./modDefinitions/modDefinitionHooks.ts", - "./extensionConsole/pages/mods/modals/shareModals/useHasEditPermissions.ts", - "./components/form/widgets/RegistryIdWidget.tsx", - "./background/partnerIntegrations.ts", - "./contentScript/pageEditor/elementPicker.ts", - "./background/requests.ts", - "./background/refreshToken.ts", - "./background/auth/getToken.ts", - "./bricks/transformers/controlFlow/Retry.ts", - "./bricks/effects/AddQuickBarAction.tsx", - "./background/browserAction.ts", - "./sidebar/FormBody.tsx", - "./extensionConsole/pages/InvitationBanner.tsx", - "./components/floatingActions/FloatingActions.tsx", - "./components/addBlockModal/useBlockSearch.ts", - "./components/addBlockModal/TagSearchInput.tsx", - "./bricks/transformers/temporaryInfo/useTemporaryPanelDefinition.ts", - "./testUtils/factories/integrationFactories.ts", - "./extensionConsole/pages/settings/LoggingSettings.tsx", - "./extensionConsole/pages/settings/GeneralSettings.tsx", - "./extensionConsole/pages/settings/ExperimentalSettings.tsx", - "./extensionConsole/pages/brickEditor/useLogContext.ts", - "./testUtils/factories/registryFactories.ts", - "./pageEditor/tabs/logs/useLogsBadgeState.ts", - "./pageEditor/tabs/editTab/editorNodes/brickNode/BrickNodeContent.tsx", - "./hooks/useInterval.ts", - "./components/fields/schemaFields/selectFieldUtils.ts", - "./components/fields/schemaFields/widgets/SchemaSelectWidget.tsx", - "./extensionConsole/pages/mods/ModsPageToolbar.tsx", - "./components/tabLayout/EditorTabLayout.tsx", - "./mods/hooks/useDeleteExtensionAction.ts", - "./extensionConsole/pages/mods/hooks/useViewShareAction.ts", - "./extensionConsole/pages/mods/hooks/useViewPublishAction.ts", - "./extensionConsole/pages/mods/hooks/useViewLogsAction.ts", - "./contrib/google/sheets/core/useSpreadsheetId.ts", - "./contrib/google/sheets/bricks/append.ts", - "./contentScript/integrations/bannerDomController.tsx", "../end-to-end-tests/auth.setup.ts", "../end-to-end-tests/env.ts", "../end-to-end-tests/fixtures/extensionBase.ts", @@ -97,12 +59,14 @@ "./background/auth/authHelpers.ts", "./background/auth/authStorage.ts", "./background/auth/codeGrantFlow.ts", + "./background/auth/getToken.ts", "./background/auth/implicitGrantFlow.ts", "./background/auth/launchInteractiveOAuth2Flow", "./background/auth/launchInteractiveOAuth2Flow.ts", "./background/auth/launchOAuth2Flow.ts", "./background/axiosFetch.ts", "./background/backgroundDomWatcher.ts", + "./background/browserAction.ts", "./background/capture.ts", "./background/clipboard.ts", "./background/contentScript.ts", @@ -113,8 +77,11 @@ "./background/messenger/strict/api.ts", "./background/messenger/strict/registration.ts", "./background/navigation.ts", + "./background/partnerIntegrations.ts", "./background/proxyUtils.ts", "./background/refreshRegistries.ts", + "./background/refreshToken.ts", + "./background/requests.ts", "./background/setToolbarBadge.test.ts", "./background/setToolbarIconFromTheme.test.ts", "./background/setToolbarIconFromTheme.ts", @@ -124,6 +91,7 @@ "./background/walkthroughModalTrigger.ts", "./bricks/available.ts", "./bricks/effects/AddDynamicTextSnippet.ts", + "./bricks/effects/AddQuickBarAction.tsx", "./bricks/effects/AddTextSnippets.ts", "./bricks/effects/CancelEffect.ts", "./bricks/effects/CancelEphemeralElements.ts", @@ -197,6 +165,7 @@ "./bricks/transformers/controlFlow/ForEach.ts", "./bricks/transformers/controlFlow/ForEachElement.ts", "./bricks/transformers/controlFlow/MapValues.ts", + "./bricks/transformers/controlFlow/Retry.ts", "./bricks/transformers/controlFlow/Run.ts", "./bricks/transformers/controlFlow/TryExcept.ts", "./bricks/transformers/convertDocument.ts", @@ -222,6 +191,7 @@ "./bricks/transformers/temporaryInfo/messenger/api.ts", "./bricks/transformers/temporaryInfo/messenger/registration.ts", "./bricks/transformers/temporaryInfo/receiverProtocol.ts", + "./bricks/transformers/temporaryInfo/useTemporaryPanelDefinition.ts", "./bricks/transformers/tourStep/overlay.ts", "./bricks/transformers/tourStep/tourStep.ts", "./bricks/transformers/traverseElements.ts", @@ -264,10 +234,12 @@ "./components/TooltipIconButton.tsx", "./components/UnstyledButton.tsx", "./components/addBlockModal/TagList.tsx", + "./components/addBlockModal/TagSearchInput.tsx", "./components/addBlockModal/addBlockModalConstants.ts", "./components/addBlockModal/addBlockModalHelpers.ts", "./components/addBlockModal/addBlockModalTypes.ts", "./components/addBlockModal/groupListingsByTag.ts", + "./components/addBlockModal/useBlockSearch.ts", "./components/annotationAlert/FieldAnnotationAlert.tsx", "./components/asyncCard/AsyncCard.tsx", "./components/asyncIcon.ts", @@ -300,6 +272,8 @@ "./components/fields/schemaFields/integrations/IntegrationAuthSelectWidget.tsx", "./components/fields/schemaFields/optionIcon/OptionIcon.tsx", "./components/fields/schemaFields/schemaUtils.ts", + "./components/fields/schemaFields/selectFieldUtils.ts", + "./components/fields/schemaFields/widgets/SchemaSelectWidget.tsx", "./components/fields/schemaFields/widgets/WidgetLoadingIndicator.tsx", "./components/fields/schemaFields/widgets/cssClassWidgets/types.ts", "./components/fields/schemaFields/widgets/cssClassWidgets/utils.ts", @@ -307,6 +281,7 @@ "./components/fields/schemaFields/widgets/varPopup/popoverTheme.ts", "./components/fields/schemaFields/widgets/varPopup/utils.ts", "./components/fields/schemaFields/widgets/widgetUtils.ts", + "./components/floatingActions/FloatingActions.tsx", "./components/floatingActions/QuickbarButton.tsx", "./components/floatingActions/floatingActionsConstants.ts", "./components/floatingActions/store.ts", @@ -318,6 +293,7 @@ "./components/form/popoverInfoLabel/PopoverInfoLabel.tsx", "./components/form/widgets/AsyncRemoteSelectWidget.tsx", "./components/form/widgets/KeyNameWidget.tsx", + "./components/form/widgets/RegistryIdWidget.tsx", "./components/form/widgets/RemoteMultiSelectWidget.tsx", "./components/form/widgets/RemoteSelectWidget.tsx", "./components/form/widgets/SelectWidget.tsx", @@ -369,6 +345,7 @@ "./components/schemaTree/SchemaTree.tsx", "./components/selectionToolPopover/SelectionToolPopover.tsx", "./components/selectionToolPopover/showSelectionToolPopover.tsx", + "./components/tabLayout/EditorTabLayout.tsx", "./components/walkthroughModal/showWalkthroughModal.ts", "./contentScript/activationConstants.ts", "./contentScript/browserActionInstantHandler.ts", @@ -380,12 +357,14 @@ "./contentScript/ephemeralPanelController.tsx", "./contentScript/focusCaptureDialog.ts", "./contentScript/integrations/LoginBanners.tsx", + "./contentScript/integrations/bannerDomController.tsx", "./contentScript/integrations/deferredLoginTypes.ts", "./contentScript/messenger/runBrickTypes.ts", "./contentScript/messenger/strict/api.ts", "./contentScript/messenger/strict/registration.ts", "./contentScript/modalDom.tsx", "./contentScript/pageEditor/beautify.ts", + "./contentScript/pageEditor/elementPicker.ts", "./contentScript/popoverDom.ts", "./contentScript/ready.ts", "./contentScript/setExtensionIdInApp.ts", @@ -425,6 +404,7 @@ "./contrib/ckeditor/ckeditorDom.ts", "./contrib/ckeditor/ckeditorProtocol.ts", "./contrib/draftjs/draftJsDom.ts", + "./contrib/google/sheets/bricks/append.ts", "./contrib/google/sheets/bricks/lookup.ts", "./contrib/google/sheets/core/getSheetIdIntegrationOutputKey.ts", "./contrib/google/sheets/core/handleGoogleRequestRejection.ts", @@ -434,6 +414,7 @@ "./contrib/google/sheets/core/types.ts", "./contrib/google/sheets/core/useGoogleAccount.ts", "./contrib/google/sheets/core/useOnChangeEffect.ts", + "./contrib/google/sheets/core/useSpreadsheetId.ts", "./contrib/hubspot/upsert.ts", "./contrib/navigationRules.ts", "./contrib/pipedrive/create.ts", @@ -491,28 +472,39 @@ "./extensionConsole/Sidebar.tsx", "./extensionConsole/SidebarLink.tsx", "./extensionConsole/components/ServiceFieldError.tsx", + "./extensionConsole/pages/InvitationBanner.tsx", "./extensionConsole/pages/activateExtension/activateTypes.ts", "./extensionConsole/pages/activateMod/IntegrationDescriptor.tsx", "./extensionConsole/pages/activateMod/UrlPermissionsList.tsx", "./extensionConsole/pages/brickEditor/CodeEditor.tsx", "./extensionConsole/pages/brickEditor/referenceTab/DetailSection.tsx", + "./extensionConsole/pages/brickEditor/useLogContext.ts", "./extensionConsole/pages/integrations/ConnectExtensionCard.tsx", "./extensionConsole/pages/integrations/ZapierIntegrationModal.tsx", + "./extensionConsole/pages/mods/ModsPageToolbar.tsx", "./extensionConsole/pages/mods/emptyView/EmptyView.tsx", "./extensionConsole/pages/mods/gridView/GridCardErrorBoundary.tsx", "./extensionConsole/pages/mods/hooks/useActivateAction.ts", "./extensionConsole/pages/mods/hooks/useReactivateAction.ts", "./extensionConsole/pages/mods/hooks/useShowPublishUrlEffect.tsx", + "./extensionConsole/pages/mods/hooks/useViewLogsAction.ts", + "./extensionConsole/pages/mods/hooks/useViewPublishAction.ts", + "./extensionConsole/pages/mods/hooks/useViewShareAction.ts", "./extensionConsole/pages/mods/labels/LastUpdatedLabel.tsx", "./extensionConsole/pages/mods/labels/SharingLabel.tsx", "./extensionConsole/pages/mods/listView/ListGroupHeader.tsx", "./extensionConsole/pages/mods/listView/ListItemErrorBoundary.tsx", "./extensionConsole/pages/mods/modals/modModalsSlice.ts", + "./extensionConsole/pages/mods/modals/shareModals/ModOwnerLabel.tsx", + "./extensionConsole/pages/mods/modals/shareModals/useHasEditPermissions.ts", "./extensionConsole/pages/mods/modals/shareModals/useSortOrganizations.ts", "./extensionConsole/pages/mods/modsPageSelectors.ts", "./extensionConsole/pages/mods/modsPageSlice.ts", "./extensionConsole/pages/onboarding/DefaultSetupCard.tsx", "./extensionConsole/pages/onboarding/partner/partnerOnboardingUtils.ts", + "./extensionConsole/pages/settings/ExperimentalSettings.tsx", + "./extensionConsole/pages/settings/GeneralSettings.tsx", + "./extensionConsole/pages/settings/LoggingSettings.tsx", "./extensionConsole/pages/settings/PrivacySettings.tsx", "./extensionConsole/pages/settings/SettingToggle.tsx", "./extensionConsole/pages/useRegistryIdParam.ts", @@ -541,6 +533,7 @@ "./hooks/useEventListener.test.ts", "./hooks/useEventListener.ts", "./hooks/useFlags.ts", + "./hooks/useInterval.ts", "./hooks/useIsEnterpriseUser.ts", "./hooks/useIsMounted.ts", "./hooks/useKeyboardShortcut.ts", @@ -595,6 +588,7 @@ "./layout/Footer.tsx", "./layout/Page.tsx", "./modDefinitions/modDefinitionConstants.ts", + "./modDefinitions/modDefinitionHooks.ts", "./modDefinitions/modDefinitionRawApiCalls.ts", "./modDefinitions/modDefinitionsListenerMiddleware.ts", "./modDefinitions/modDefinitionsSelectors.ts", @@ -605,6 +599,7 @@ "./modDefinitions/util/pickModDefinitionMetadata.ts", "./mods/ModIcon.tsx", "./mods/ModIntegrationsContext.tsx", + "./mods/hooks/useDeleteExtensionAction.ts", "./mods/hooks/useMarketplaceUrl.ts", "./mv3/SessionStorage.ts", "./mv3/api.ts", @@ -655,10 +650,12 @@ "./pageEditor/tabs/editTab/editorNodes/PipelineHeaderNode.tsx", "./pageEditor/tabs/editTab/editorNodes/PipelineOffsetView.tsx", "./pageEditor/tabs/editTab/editorNodes/TrailingMessage.tsx", + "./pageEditor/tabs/editTab/editorNodes/brickNode/BrickNodeContent.tsx", "./pageEditor/tabs/editTab/editorNodes/brickNode/MoveBrickControl.tsx", "./pageEditor/tabs/editTab/editorNodes/nodeActions/NodeActionsView.tsx", "./pageEditor/tabs/effect/AdvancedLinks.tsx", "./pageEditor/tabs/effect/configurationConstants.ts", + "./pageEditor/tabs/logs/useLogsBadgeState.ts", "./pageEditor/uiState/uiState.ts", "./pageEditor/uiState/uiStateTypes.ts", "./pageScript/frameworks/component.ts", @@ -712,6 +709,7 @@ "./sandbox/messenger/registration.ts", "./sandbox/sandbox.ts", "./sidebar/DefaultPanel.tsx", + "./sidebar/FormBody.tsx", "./sidebar/Header.tsx", "./sidebar/RendererComponent.tsx", "./sidebar/SidebarErrorBoundary.tsx", @@ -792,9 +790,11 @@ "./testUtils/factories/browserFactories.ts", "./testUtils/factories/databaseFactories.ts", "./testUtils/factories/domFactories.ts", + "./testUtils/factories/integrationFactories.ts", "./testUtils/factories/logFactories.ts", "./testUtils/factories/messengerFactories.ts", "./testUtils/factories/metadataFactory.ts", + "./testUtils/factories/registryFactories.ts", "./testUtils/factories/selectorFactories.ts", "./testUtils/factories/sidebarEntryFactories.ts", "./testUtils/factories/stringFactories.ts", @@ -811,10 +811,10 @@ "./themes/themeUtils.ts", "./tinyPages/RestrictedUrlPopupApp.tsx", "./tinyPages/devtools.ts", + "./tinyPages/offscreen.ts", "./tinyPages/restrictedUrlPopup.tsx", "./tinyPages/restrictedUrlPopupConstants.ts", "./tinyPages/restrictedUrlPopupUtils.ts", - "./tinyPages/offscreen.ts", "./tours/tourRunDatabase.ts", "./types/annotationTypes.ts", "./types/brickTypes.ts", @@ -916,6 +916,7 @@ "./utils/variableUtils.ts", "./validators/formValidator.ts", "./validators/schemaValidator.ts", + "./vendors/Overlay.tsx", "./vendors/encodings.ts", "./vendors/hoverintent.d.ts", "./vendors/jQueryInitialize.d.ts", From a51514ff528a26c1b961ffa22393791353b0ddb0 Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 19:09:47 -0400 Subject: [PATCH 09/68] src/extensionConsole/pages/mods/GetStartedView.tsx --- src/extensionConsole/pages/mods/GetStartedView.tsx | 8 ++++---- src/tsconfig.strictNullChecks.json | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/extensionConsole/pages/mods/GetStartedView.tsx b/src/extensionConsole/pages/mods/GetStartedView.tsx index 0d87fb7827..f471830c39 100644 --- a/src/extensionConsole/pages/mods/GetStartedView.tsx +++ b/src/extensionConsole/pages/mods/GetStartedView.tsx @@ -55,7 +55,7 @@ const GetStartedView: React.VoidFunctionComponent<{ const onboardingModId = getMilestone("first_time_public_blueprint_install") ?.metadata?.blueprintId as RegistryId; - const { data: recipe, isFetching: isFetchingRecipe } = + const { data: modDefinition, isFetching: isFetchingRecipe } = useOptionalModDefinition(onboardingModId); const { data: listings } = useGetMarketplaceListingsQuery( @@ -77,13 +77,13 @@ const GetStartedView: React.VoidFunctionComponent<{

Success!{" "} - {!isFetchingRecipe && ( + {!isFetchingRecipe && modDefinition && ( <> - {" "} + {" "} )} {" "} is ready to use. diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index 4db67db701..5b129d1b18 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -4,6 +4,7 @@ "strictNullChecks": true }, "files": [ + "./extensionConsole/pages/mods/GetStartedView.tsx", "../end-to-end-tests/auth.setup.ts", "../end-to-end-tests/env.ts", "../end-to-end-tests/fixtures/extensionBase.ts", From 48fc706a3f16a970bb2b8b4a0d7379252fdf19a8 Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 19:46:14 -0400 Subject: [PATCH 10/68] src/mods/useModViewItems.tsx --- src/mods/useModViewItems.tsx | 100 ++++++++++++++++++----------- src/tsconfig.strictNullChecks.json | 1 + src/types/modTypes.ts | 6 +- src/utils/modUtils.ts | 6 +- 4 files changed, 70 insertions(+), 43 deletions(-) diff --git a/src/mods/useModViewItems.tsx b/src/mods/useModViewItems.tsx index 67a5d75117..bca649c73d 100644 --- a/src/mods/useModViewItems.tsx +++ b/src/mods/useModViewItems.tsx @@ -37,79 +37,106 @@ import { useGetMarketplaceListingsQuery } from "@/data/service/api"; import { selectOrganizations, selectScope } from "@/auth/authSelectors"; import { isDeploymentActive } from "@/utils/deploymentUtils"; import { useAllModDefinitions } from "@/modDefinitions/modDefinitionHooks"; +import { compact } from "lodash"; +import { RegistryId } from "@/types/registryTypes"; +import { ActivatedModComponent } from "@/types/modComponentTypes"; function useModViewItems(mods: Mod[]): { modViewItems: readonly ModViewItem[]; isLoading: boolean; } { const scope = useSelector(selectScope); - const installedExtensions = useSelector(selectActivatedModComponents); + const activatedModComponents = useSelector(selectActivatedModComponents); const organizations = useSelector(selectOrganizations); // Don't merge async states. Allow hook to render without listings - const listingsQuery = useGetMarketplaceListingsQuery(); - const { data: recipes, isLoading: isRecipesLoading } = useAllModDefinitions(); + const { data: listings } = useGetMarketplaceListingsQuery(); + const { data: modDefinitions, isLoading: isRecipesLoading } = + useAllModDefinitions(); - const { installedExtensionIds, installedRecipeIds } = useMemo( + const { activatedModComponentIds, activatedModIds } = useMemo( () => ({ - installedExtensionIds: new Set( - installedExtensions.map((extension) => extension.id), + activatedModComponentIds: new Set( + activatedModComponents.map((extension) => extension.id), ), - installedRecipeIds: new Set( - installedExtensions.map((extension) => extension._recipe?.id), + activatedModIds: new Set( + compact( + activatedModComponents.map((extension) => extension._recipe?.id), + ), ), }), - [installedExtensions], + [activatedModComponents], ); const isActive = useCallback( (mod: Mod) => { if (isResolvedModComponent(mod)) { - return installedExtensionIds.has(mod.id); + return activatedModComponentIds.has(mod.id); } - return installedRecipeIds.has(mod.metadata.id); + return activatedModIds.has(mod.metadata.id); }, - [installedExtensionIds, installedRecipeIds], + [activatedModComponentIds, activatedModIds], ); const getStatus = useCallback( (mod: Mod): ModStatus => { - if (isDeployment(mod, installedExtensions)) { + if (isDeployment(mod, activatedModComponents)) { if (isResolvedModComponent(mod)) { return isDeploymentActive(mod) ? "Active" : "Paused"; } - const deploymentExtension = installedExtensions.find( - (installedExtension) => - installedExtension._recipe?.id === getPackageId(mod) && - installedExtension._deployment, + const componentFromDeployment = activatedModComponents.find( + (activatedModComponent) => + activatedModComponent._recipe?.id === getPackageId(mod) && + activatedModComponent._deployment, ); + if (!componentFromDeployment) { + return "Inactive"; + } - return isDeploymentActive(deploymentExtension) ? "Active" : "Paused"; + return isDeploymentActive(componentFromDeployment) + ? "Active" + : "Paused"; } return isActive(mod) ? "Active" : "Inactive"; }, - [installedExtensions, isActive], + [activatedModComponents, isActive], ); const modViewItems = useMemo(() => { - // Load to map for fast lookup if you have a lot of recipes. Could put in its own memo - const recipeMap = new Map( - (recipes ?? []).map((recipe) => [recipe.metadata.id, recipe]), + // Load to map for fast lookup if you have a lot of modDefinitions. Could put in its own memo + const modDefinitionMap = new Map( + (modDefinitions ?? []).map((modDefinition) => [ + modDefinition.metadata.id, + modDefinition, + ]), ); - // Pick any ModComponentBase from the blueprint to check for updates. All of their versions should be the same. - const extensionsMap = new Map( - installedExtensions - .filter((x) => x._recipe?.id) - .map((extension) => [extension._recipe.id, extension]), - ); + const modComponentEntries: Array<[RegistryId, ActivatedModComponent]> = + compact( + activatedModComponents.map((modComponent) => { + if (modComponent._recipe) { + return [modComponent._recipe.id, modComponent]; + } + + return null; + }), + ); + + // Pick any ModComponentBase from the mod to check for updates. All of their versions should be the same. + const extensionsMap = new Map(modComponentEntries); return mods.map((mod) => { const packageId = getPackageId(mod); + let listingId: UUID | null = null; + if (packageId && listings) { + // eslint-disable-next-line security/detect-object-injection -- packageId is a registry id + listingId = listings[packageId]?.id ?? null; + } + return { name: getLabel(mod), description: getDescription(mod), @@ -119,16 +146,15 @@ function useModViewItems(mods: Mod[]): { mod, organizations, scope, - installedExtensions, + installedExtensions: activatedModComponents, }), - // eslint-disable-next-line security/detect-object-injection -- packageId is a registry id - listingId: listingsQuery.data?.[packageId]?.id, + listingId, }, updatedAt: getUpdatedAt(mod), status: getStatus(mod), - hasUpdate: updateAvailable(recipeMap, extensionsMap, mod), + hasUpdate: updateAvailable(modDefinitionMap, extensionsMap, mod), installedVersionNumber: getInstalledVersionNumber( - installedExtensions, + activatedModComponents, mod, ), unavailable: isUnavailableMod(mod), @@ -136,13 +162,13 @@ function useModViewItems(mods: Mod[]): { } satisfies ModViewItem; }); }, [ - getStatus, + modDefinitions, + activatedModComponents, mods, - installedExtensions, - listingsQuery, + listings, organizations, - recipes, scope, + getStatus, ]); return { diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index 5b129d1b18..12c36b49e3 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -4,6 +4,7 @@ "strictNullChecks": true }, "files": [ + "./mods/useModViewItems.tsx", "./extensionConsole/pages/mods/GetStartedView.tsx", "../end-to-end-tests/auth.setup.ts", "../end-to-end-tests/env.ts", diff --git a/src/types/modTypes.ts b/src/types/modTypes.ts index 25b451f70d..03105b3dd6 100644 --- a/src/types/modTypes.ts +++ b/src/types/modTypes.ts @@ -59,14 +59,14 @@ export type ModViewItem = { name: string; description: string; sharing: { - packageId: RegistryId; + packageId: RegistryId | undefined; source: SharingSource; listingId: string | null; }; - updatedAt: string; + updatedAt: string | null; status: ModStatus; hasUpdate: boolean; - installedVersionNumber: string; + installedVersionNumber: string | undefined; // Used to get Mod actions from useModActions mod: Mod; /** diff --git a/src/utils/modUtils.ts b/src/utils/modUtils.ts index 9de141bea8..c9cea63e1d 100644 --- a/src/utils/modUtils.ts +++ b/src/utils/modUtils.ts @@ -37,7 +37,7 @@ import { import { type RegistryId } from "@/types/registryTypes"; import { type UUID } from "@/types/stringTypes"; import { InvalidTypeError } from "@/errors/genericErrors"; -import { assertNotNullish } from "./nullishUtils"; +import { assertNotNullish, Nullishable } from "./nullishUtils"; import { minimalSchemaFactory, minimalUiSchemaFactory, @@ -160,7 +160,7 @@ function hasRegistryScope( * @param mod the mod * @param userScope the user's scope, or null if it's not set */ -function isPersonal(mod: Mod, userScope: string | null): boolean { +function isPersonal(mod: Mod, userScope: Nullishable): boolean { if (isResolvedModComponent(mod)) { return ( isPersonalModComponent(mod) || @@ -209,7 +209,7 @@ export function getSharingSource({ }: { mod: Mod; organizations: Organization[]; - scope: string; + scope: Nullishable; installedExtensions: UnresolvedModComponent[]; }): SharingSource { let sharingType: SharingType | null = null; From b5b5427d00f471e74858c003b9927c83cad5bbeb Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 19:58:03 -0400 Subject: [PATCH 11/68] test memo compare dependencies --- src/hooks/useMemoCompare.test.ts | 48 ++++++++++++++++++++++++++++++++ src/hooks/useMemoCompare.ts | 4 +-- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/hooks/useMemoCompare.test.ts b/src/hooks/useMemoCompare.test.ts index 24be989cb8..022b74e592 100644 --- a/src/hooks/useMemoCompare.test.ts +++ b/src/hooks/useMemoCompare.test.ts @@ -53,4 +53,52 @@ describe("useMemoCompare", () => { expect(result.current).not.toBe(initial); }); + + it("returns same reference when dependencies don't change", async () => { + const initial = { + values: { foo: 42 }, + dependencies: [42, "foo"], + }; + + const { result, rerender } = renderHook( + ({ values, dependencies }) => + useMemoCompare(values, deepEquals, dependencies), + { + initialProps: initial, + }, + ); + + expect(result.current).toBe(initial.values); + + rerender({ + values: { foo: 42 }, + dependencies: [42, "foo"], + }); + + expect(result.current).toBe(initial.values); + }); + + it("returns different reference when dependencies change", async () => { + const initial = { + values: { foo: 42 }, + dependencies: [42, "foo"], + }; + + const { result, rerender } = renderHook( + ({ values, dependencies }) => + useMemoCompare(values, deepEquals, dependencies), + { + initialProps: initial, + }, + ); + + expect(result.current).toBe(initial.values); + + rerender({ + values: { foo: 42 }, + dependencies: [42, "bar"], + }); + + expect(result.current).not.toBe(initial.values); + }); }); diff --git a/src/hooks/useMemoCompare.ts b/src/hooks/useMemoCompare.ts index a82718fce2..6fd9573272 100644 --- a/src/hooks/useMemoCompare.ts +++ b/src/hooks/useMemoCompare.ts @@ -50,9 +50,7 @@ function useMemoCompare( let isDependenciesEqual: boolean; if (dependencies === undefined && previousDependencies === undefined) { isDependenciesEqual = true; - } - - if (dependencies === undefined || previousDependencies === undefined) { + } else if (dependencies === undefined || previousDependencies === undefined) { isDependenciesEqual = false; } else { isDependenciesEqual = deepEquals(previousDependencies, dependencies); From e7be3b664641a20801f143765ad149143d2d5a7d Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 19:59:37 -0400 Subject: [PATCH 12/68] fix scopeAndId tests --- src/utils/registryUtils.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/utils/registryUtils.test.ts b/src/utils/registryUtils.test.ts index e948ab59f4..0530062140 100644 --- a/src/utils/registryUtils.test.ts +++ b/src/utils/registryUtils.test.ts @@ -45,25 +45,25 @@ describe("generatePackageId", () => { describe("getScopeAndId", () => { test("normal id", () => { const id = "@foo/bar" as RegistryId; - expect(getScopeAndId(id)).toStrictEqual(["@foo", "bar"]); + expect(getScopeAndId(id)).toStrictEqual({ scope: "@foo", id: "bar" }); }); test("id with slash", () => { const id = "@foo/bar/baz" as RegistryId; - expect(getScopeAndId(id)).toStrictEqual(["@foo", "bar/baz"]); + expect(getScopeAndId(id)).toStrictEqual({ scope: "@foo", id: "bar/baz" }); }); test("id without scope", () => { const id = "foobar" as RegistryId; - expect(getScopeAndId(id)).toStrictEqual([undefined, "foobar"]); + expect(getScopeAndId(id)).toStrictEqual({ scope: undefined, id: "foobar" }); }); test("id without scope with slash", () => { const id = "foo/bar/baz" as RegistryId; - expect(getScopeAndId(id)).toStrictEqual([undefined, "foo/bar/baz"]); + expect(getScopeAndId(id)).toStrictEqual({ + scope: undefined, + id: "foo/bar/baz", + }); }); test("scope without id", () => { const id = "@foo" as RegistryId; - expect(getScopeAndId(id)).toStrictEqual(["@foo", undefined]); - }); - test("id is nullish", () => { - expect(getScopeAndId(null)).toStrictEqual([undefined, undefined]); + expect(getScopeAndId(id)).toStrictEqual({ scope: "@foo", id: undefined }); }); }); From f43adb60bf3045b504f10dab672a2fac53b9aa77 Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 20:13:08 -0400 Subject: [PATCH 13/68] autosuggest --- .../CreatableAutosuggest.tsx | 16 ++++++++++++---- src/tsconfig.strictNullChecks.json | 1 + 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/pageEditor/fields/creatableAutosuggest/CreatableAutosuggest.tsx b/src/pageEditor/fields/creatableAutosuggest/CreatableAutosuggest.tsx index 408029ce26..b31f0acba6 100644 --- a/src/pageEditor/fields/creatableAutosuggest/CreatableAutosuggest.tsx +++ b/src/pageEditor/fields/creatableAutosuggest/CreatableAutosuggest.tsx @@ -42,9 +42,13 @@ interface CreateNew extends SuggestionTypeBase { } function isNew( - suggestion: T | CreateNew, + suggestion: T | CreateNew | null, ): suggestion is CreateNew { - return suggestion && "isNew" in suggestion; + if (suggestion == null) { + return false; + } + + return "isNew" in suggestion; } export interface Props { @@ -157,7 +161,7 @@ const CreatableAutosuggest = ({ (suggestion: SuggestionType | CreateNew) => isNew(suggestion) ? // TODO: Fix or drop https://github.com/pixiebrix/pixiebrix-extension/issues/6686 - renderCreateNew(`Create "${suggestion.value}"`) + renderCreateNew?.(`Create "${suggestion.value}"`) : renderSuggestion(suggestion), [renderCreateNew, renderSuggestion], ); @@ -187,7 +191,11 @@ const CreatableAutosuggest = ({ SuggestionType | CreateNew > = (event, data) => { if (isNew(data.suggestion)) { - const newSuggestion = onCreateNew(data.suggestionValue); + const newSuggestion = onCreateNew?.(data.suggestionValue); + if (newSuggestion == null) { + return; + } + setCreatedSuggestions((existing) => [...existing, newSuggestion]); onSuggestionSelected(newSuggestion); } else { diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index 12c36b49e3..5a2a912900 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -4,6 +4,7 @@ "strictNullChecks": true }, "files": [ + "./pageEditor/fields/creatableAutosuggest/CreatableAutosuggest.tsx", "./mods/useModViewItems.tsx", "./extensionConsole/pages/mods/GetStartedView.tsx", "../end-to-end-tests/auth.setup.ts", From 5eaf7ee05692558e03facb8c3373c83927400ebc Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 20:16:12 -0400 Subject: [PATCH 14/68] src/background/backgroundPlatform.ts --- src/background/messenger/api.ts | 3 ++- src/background/requests.ts | 3 ++- src/tsconfig.strictNullChecks.json | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/background/messenger/api.ts b/src/background/messenger/api.ts index ecaff620c1..dd7d32d7ac 100644 --- a/src/background/messenger/api.ts +++ b/src/background/messenger/api.ts @@ -24,6 +24,7 @@ import { import type { NetworkRequestConfig } from "@/types/networkTypes"; import type { RemoteResponse } from "@/types/contract"; import { type SanitizedIntegrationConfig } from "@/integrations/integrationTypes"; +import { Nullishable } from "@/utils/nullishUtils"; export const getAvailableVersion = getMethod("GET_AVAILABLE_VERSION", bg); export const getPartnerPrincipals = getMethod("GET_PARTNER_PRINCIPALS", bg); @@ -63,7 +64,7 @@ export const performConfiguredRequestInBackground = getMethod( "CONFIGURED_REQUEST", bg, ) as ( - integrationConfig: SanitizedIntegrationConfig | null, + integrationConfig: Nullishable, requestConfig: NetworkRequestConfig, options: { interactiveLogin: boolean }, ) => Promise>; diff --git a/src/background/requests.ts b/src/background/requests.ts index 3a8748e770..10c9bab3d8 100644 --- a/src/background/requests.ts +++ b/src/background/requests.ts @@ -64,6 +64,7 @@ import { } from "@/integrations/constants"; import { memoizeUntilSettled } from "@/utils/promiseUtils"; import { pixiebrixConfigurationFactory } from "@/integrations/util/pixiebrixConfigurationFactory"; +import { Nullishable } from "@/utils/nullishUtils"; // Firefox won't send response objects from the background page to the content script. Strip out the // potentially sensitive parts of the response (the request, headers, etc.) @@ -386,7 +387,7 @@ async function getIntegrationMessageContext( export async function performConfiguredRequest( // Note: This signature is ignored by `webext-messenger` due to the generic, // so it must be copied into `background/messenger/api.ts` - integrationConfig: SanitizedIntegrationConfig | null, + integrationConfig: Nullishable, requestConfig: NetworkRequestConfig, options: { interactiveLogin: boolean }, ): Promise> { diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index 5a2a912900..8613bc72d1 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -4,6 +4,7 @@ "strictNullChecks": true }, "files": [ + "./background/backgroundPlatform.ts", "./pageEditor/fields/creatableAutosuggest/CreatableAutosuggest.tsx", "./mods/useModViewItems.tsx", "./extensionConsole/pages/mods/GetStartedView.tsx", From ca34895cc95463b8f0a0e3eb96e7522576664883 Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 20:18:55 -0400 Subject: [PATCH 15/68] src/background/restrictUnauthenticatedUrlAccess.ts --- src/background/restrictUnauthenticatedUrlAccess.ts | 4 ++++ src/tsconfig.strictNullChecks.json | 1 + 2 files changed, 5 insertions(+) diff --git a/src/background/restrictUnauthenticatedUrlAccess.ts b/src/background/restrictUnauthenticatedUrlAccess.ts index bf072ec191..ad973ff4e4 100644 --- a/src/background/restrictUnauthenticatedUrlAccess.ts +++ b/src/background/restrictUnauthenticatedUrlAccess.ts @@ -70,6 +70,10 @@ async function getAuthUrlPatterns(organizationId: UUID): Promise { async function isRestrictedUrl(url: string): Promise { const cachedAuthUrlPatterns = await authUrlPatternCache.get(); + if (!cachedAuthUrlPatterns) { + return false; + } + return testMatchPatterns(cachedAuthUrlPatterns, url); } diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index 8613bc72d1..f5b91babe9 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -4,6 +4,7 @@ "strictNullChecks": true }, "files": [ + "./background/restrictUnauthenticatedUrlAccess.ts", "./background/backgroundPlatform.ts", "./pageEditor/fields/creatableAutosuggest/CreatableAutosuggest.tsx", "./mods/useModViewItems.tsx", From 8e45b83593b0b553e92d3115d7542b7e6cd7adbf Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 20:28:05 -0400 Subject: [PATCH 16/68] src/bricks/effects/insertHtml.ts --- src/bricks/effects/insertHtml.ts | 7 ++++++- src/tsconfig.strictNullChecks.json | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/bricks/effects/insertHtml.ts b/src/bricks/effects/insertHtml.ts index bf7698f8aa..40cd09ce38 100644 --- a/src/bricks/effects/insertHtml.ts +++ b/src/bricks/effects/insertHtml.ts @@ -94,6 +94,10 @@ class InsertHtml extends EffectABC { const sanitizedHTML = sanitize(html); const sanitizedElement = $(sanitizedHTML).get(0); + if (sanitizedElement == null) { + throw new BusinessError("Invalid HTML element"); + } + const escapedId = escape(replacementId); if (replacementId) { // Use PIXIEBRIX_DATA_ATTR instead of id to support semantics of multiple elements on the page @@ -106,7 +110,8 @@ class InsertHtml extends EffectABC { for (const anchorElement of anchorElements.get()) { try { anchorElement.insertAdjacentHTML( - POSITION_MAP.get(position), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion,@typescript-eslint/no-unnecessary-type-assertion -- map is exhaustive + POSITION_MAP.get(position)!, sanitizedElement.outerHTML, ); } catch (error) { diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index f5b91babe9..8ae02c3d5e 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -4,6 +4,7 @@ "strictNullChecks": true }, "files": [ + "./bricks/effects/insertHtml.ts", "./background/restrictUnauthenticatedUrlAccess.ts", "./background/backgroundPlatform.ts", "./pageEditor/fields/creatableAutosuggest/CreatableAutosuggest.tsx", From 2cf58763c450877a6c47ecf3d74739f54e2e9604 Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 20:34:11 -0400 Subject: [PATCH 17/68] ./bricks/effects/runSubTour.ts --- src/bricks/effects/runSubTour.ts | 3 +-- src/tsconfig.strictNullChecks.json | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bricks/effects/runSubTour.ts b/src/bricks/effects/runSubTour.ts index 8cafb1be48..a34d0a19f4 100644 --- a/src/bricks/effects/runSubTour.ts +++ b/src/bricks/effects/runSubTour.ts @@ -19,7 +19,6 @@ import { EffectABC } from "@/types/bricks/effectTypes"; import { type BrickArgs, type BrickOptions } from "@/types/runtimeTypes"; import { type Schema } from "@/types/schemaTypes"; import { runSubTour } from "@/starterBricks/tourController"; -import { isEmpty } from "lodash"; import { BusinessError } from "@/errors/businessErrors"; import { propertiesToSchema } from "@/utils/schemaUtils"; @@ -46,7 +45,7 @@ export class RunSubTourEffect extends EffectABC { { tour }: BrickArgs<{ tour: string }>, { logger }: BrickOptions, ): Promise { - if (isEmpty(logger.context.blueprintId)) { + if (logger.context.blueprintId == null) { throw new BusinessError("Can only run sub-tours in the same mod"); } diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index 8ae02c3d5e..758139b9de 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -4,6 +4,7 @@ "strictNullChecks": true }, "files": [ + "./bricks/effects/runSubTour.ts", "./bricks/effects/insertHtml.ts", "./background/restrictUnauthenticatedUrlAccess.ts", "./background/backgroundPlatform.ts", From dce3731a034c2b4ab9af70fa92580e0a1bb74d6d Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 20:38:29 -0400 Subject: [PATCH 18/68] ./bricks/effects/scrollIntoView.ts --- src/bricks/effects/scrollIntoView.ts | 4 ++-- src/tsconfig.strictNullChecks.json | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/bricks/effects/scrollIntoView.ts b/src/bricks/effects/scrollIntoView.ts index ca3e4949a4..5f745a3bff 100644 --- a/src/bricks/effects/scrollIntoView.ts +++ b/src/bricks/effects/scrollIntoView.ts @@ -59,7 +59,7 @@ class ScrollIntoViewEffect extends EffectABC { description: "Defines horizontal alignment", }, }, - [], + ["selector"], ); override async isRootAware(): Promise { @@ -73,7 +73,7 @@ class ScrollIntoViewEffect extends EffectABC { block = "start", inline = "nearest", }: BrickArgs<{ - selector?: string; + selector: string; behavior?: "auto" | "smooth"; block?: "start" | "center" | "end" | "nearest"; inline?: "start" | "center" | "end" | "nearest"; diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index 758139b9de..7fa856b367 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -4,6 +4,7 @@ "strictNullChecks": true }, "files": [ + "./bricks/effects/scrollIntoView.ts", "./bricks/effects/runSubTour.ts", "./bricks/effects/insertHtml.ts", "./background/restrictUnauthenticatedUrlAccess.ts", From 58816ce39d1b2f8be944c7076d601cb3c484b728 Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 21:18:22 -0400 Subject: [PATCH 19/68] src/bricks/transformers/controlFlow/WithAsyncModVariable.ts --- .../controlFlow/WithAsyncModVariable.ts | 13 +++++++++++-- src/platform/state/stateController.ts | 18 ++++++++++-------- src/runtime/extendModVariableContext.ts | 14 ++++++++------ src/tsconfig.strictNullChecks.json | 1 + 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/bricks/transformers/controlFlow/WithAsyncModVariable.ts b/src/bricks/transformers/controlFlow/WithAsyncModVariable.ts index 805ea22965..f4a98abb40 100644 --- a/src/bricks/transformers/controlFlow/WithAsyncModVariable.ts +++ b/src/bricks/transformers/controlFlow/WithAsyncModVariable.ts @@ -33,6 +33,7 @@ import { PropError } from "@/errors/businessErrors"; import { type BrickConfig } from "@/bricks/types"; import { castTextLiteralOrThrow } from "@/utils/expressionUtils"; import { propertiesToSchema } from "@/utils/schemaUtils"; +import { assertNotNullish } from "@/utils/nullishUtils"; /** * Map to keep track of the current execution nonce for each Mod Variable. Used to ignore stale request results. @@ -104,7 +105,7 @@ export class WithAsyncModVariable extends TransformerABC { ): Promise { const { stateKey } = _config.config; - let name = ""; + let name: string | null = null; try { name = castTextLiteralOrThrow(stateKey); } catch { @@ -176,6 +177,14 @@ export class WithAsyncModVariable extends TransformerABC { ) { const requestId = uuidv4(); const { blueprintId, extensionId } = logger.context; + assertNotNullish( + blueprintId, + "This brick must be inside a Mod to use a mod variable", + ); + assertNotNullish( + extensionId, + "Logger extensionId is null, something went wrong", + ); if (isNullOrBlank(stateKey)) { throw new PropError( @@ -280,7 +289,7 @@ export class WithAsyncModVariable extends TransformerABC { currentData: null, data: null, requestId, - error: serializeError(error), + error: serializeError(error) as JsonObject, }, "put", ); diff --git a/src/platform/state/stateController.ts b/src/platform/state/stateController.ts index c5bbd378ea..4e8e6f2039 100644 --- a/src/platform/state/stateController.ts +++ b/src/platform/state/stateController.ts @@ -24,6 +24,8 @@ import { assertPlatformCapability } from "@/platform/platformContext"; type MergeStrategy = "shallow" | "replace" | "deep"; +type StateNamespace = "blueprint" | "extension" | "shared"; + const privateState = new Map(); /** @@ -92,10 +94,9 @@ export function setState({ data, mergeStrategy, extensionId, - // Normalize undefined to null for lookup - blueprintId = null, + blueprintId, }: { - namespace: string; + namespace: StateNamespace; data: JsonObject; mergeStrategy: MergeStrategy; extensionId: UUID | null; @@ -143,7 +144,8 @@ export function setState({ } default: { - throw new BusinessError(`Invalid namespace: ${namespace}`); + const exhaustiveCheck: never = namespace; + throw new BusinessError(`Invalid namespace: ${exhaustiveCheck}`); } } } @@ -151,10 +153,9 @@ export function setState({ export function getState({ namespace, extensionId, - // Normalize undefined to null for lookup - blueprintId = null, + blueprintId, }: { - namespace: string; + namespace: StateNamespace; extensionId: UUID; blueprintId: RegistryId | null; }): JsonObject { @@ -178,7 +179,8 @@ export function getState({ } default: { - throw new BusinessError(`Invalid namespace: ${namespace}`); + const exhaustiveCheck: never = namespace; + throw new BusinessError(`Invalid namespace: ${exhaustiveCheck}`); } } } diff --git a/src/runtime/extendModVariableContext.ts b/src/runtime/extendModVariableContext.ts index 757ef59231..533ead1c37 100644 --- a/src/runtime/extendModVariableContext.ts +++ b/src/runtime/extendModVariableContext.ts @@ -135,12 +135,14 @@ function extendModVariableContext( // Eagerly grab the state. It's fast/synchronous since it's in memory in the same JS context. // Previously, we had considered using a proxy to lazily load the state. However, eagerly reading is simpler. // Additionally, in the future to pass the context to the sandbox we'd have to always load the state anyway. - const modState = getState({ - namespace: "blueprint", - blueprintId, - // `extensionId` is not used because namespace is `blueprint` - extensionId: validateUUID(null), - }); + const modState = blueprintId + ? getState({ + namespace: "blueprint", + blueprintId, + // `extensionId` is not used because namespace is `blueprint` + extensionId: validateUUID(null), + }) + : {}; return { ...originalContext, diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index 7fa856b367..82d75fa7d1 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -4,6 +4,7 @@ "strictNullChecks": true }, "files": [ + "./bricks/transformers/controlFlow/WithAsyncModVariable.ts", "./bricks/effects/scrollIntoView.ts", "./bricks/effects/runSubTour.ts", "./bricks/effects/insertHtml.ts", From 80d4a78506098e3a5934f63cb98a8cc19772feea Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 22:25:04 -0400 Subject: [PATCH 20/68] mod state / variable context --- src/bricks/effects/pageState.ts | 5 ++- .../controlFlow/WithAsyncModVariable.ts | 6 +--- src/platform/state/stateController.ts | 35 ++++++++++--------- src/runtime/extendModVariableContext.ts | 20 +++++------ .../pipelineTests/modVariableContext.test.ts | 6 ++-- 5 files changed, 34 insertions(+), 38 deletions(-) diff --git a/src/bricks/effects/pageState.ts b/src/bricks/effects/pageState.ts index b85d5d1c1f..62cd8cd817 100644 --- a/src/bricks/effects/pageState.ts +++ b/src/bricks/effects/pageState.ts @@ -194,13 +194,12 @@ export class GetPageState extends TransformerABC { { namespace = "blueprint" }: BrickArgs<{ namespace?: Namespace }>, { logger, platform }: BrickOptions, ): Promise { - const { blueprintId = null, extensionId } = logger.context; + const { blueprintId, extensionId } = logger.context; return platform.state.getState({ namespace, blueprintId, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion -- TODO: https://github.com/pixiebrix/pixiebrix-extension/issues/7891 - extensionId: extensionId!, + extensionId, }); } } diff --git a/src/bricks/transformers/controlFlow/WithAsyncModVariable.ts b/src/bricks/transformers/controlFlow/WithAsyncModVariable.ts index f4a98abb40..050b08bd86 100644 --- a/src/bricks/transformers/controlFlow/WithAsyncModVariable.ts +++ b/src/bricks/transformers/controlFlow/WithAsyncModVariable.ts @@ -176,11 +176,7 @@ export class WithAsyncModVariable extends TransformerABC { { logger, runPipeline }: BrickOptions, ) { const requestId = uuidv4(); - const { blueprintId, extensionId } = logger.context; - assertNotNullish( - blueprintId, - "This brick must be inside a Mod to use a mod variable", - ); + const { blueprintId = null, extensionId } = logger.context; assertNotNullish( extensionId, "Logger extensionId is null, something went wrong", diff --git a/src/platform/state/stateController.ts b/src/platform/state/stateController.ts index 4e8e6f2039..5b24ade889 100644 --- a/src/platform/state/stateController.ts +++ b/src/platform/state/stateController.ts @@ -21,6 +21,7 @@ import { cloneDeep, isEqual, merge } from "lodash"; import { BusinessError } from "@/errors/businessErrors"; import { type JsonObject } from "type-fest"; import { assertPlatformCapability } from "@/platform/platformContext"; +import { assertNotNullish, type Nullishable } from "@/utils/nullishUtils"; type MergeStrategy = "shallow" | "replace" | "deep"; @@ -71,8 +72,8 @@ function dispatchStageChangeEventOnChange({ previous: unknown; next: unknown; namespace: string; - extensionId: UUID; - blueprintId: RegistryId | null; + extensionId: Nullishable; + blueprintId: Nullishable; }) { if (!isEqual(previous, next)) { // For now, leave off the event data because we're using a public channel @@ -94,20 +95,17 @@ export function setState({ data, mergeStrategy, extensionId, - blueprintId, + // Normalize undefined to null for lookup + blueprintId = null, }: { namespace: StateNamespace; data: JsonObject; mergeStrategy: MergeStrategy; - extensionId: UUID | null; - blueprintId: RegistryId | null; + extensionId: Nullishable; + blueprintId: Nullishable; }) { assertPlatformCapability("state"); - if (extensionId == null) { - throw new Error("extensionId is required"); - } - const notifyOnChange = (previous: JsonObject, next: JsonObject) => { dispatchStageChangeEventOnChange({ previous, @@ -130,12 +128,14 @@ export function setState({ case "blueprint": { const previous = modState.get(blueprintId) ?? {}; const next = mergeState(previous, data, mergeStrategy); + console.log("setting blueprint state", blueprintId, next); modState.set(blueprintId, next); notifyOnChange(previous, next); return next; } case "extension": { + assertNotNullish(extensionId, "Invalid context: extensionId not found"); const previous = privateState.get(extensionId) ?? {}; const next = mergeState(previous, data, mergeStrategy); privateState.set(extensionId, next); @@ -153,11 +153,12 @@ export function setState({ export function getState({ namespace, extensionId, - blueprintId, + // Normalize undefined to null for lookup + blueprintId = null, }: { namespace: StateNamespace; - extensionId: UUID; - blueprintId: RegistryId | null; + extensionId: Nullishable; + blueprintId: Nullishable; }): JsonObject { assertPlatformCapability("state"); @@ -167,14 +168,16 @@ export function getState({ } case "blueprint": { + console.log( + "getting blueprint state", + blueprintId, + modState.get(blueprintId), + ); return modState.get(blueprintId) ?? {}; } case "extension": { - if (extensionId == null) { - throw new Error("Invalid context: extensionId not found"); - } - + assertNotNullish(extensionId, "Invalid context: extensionId not found"); return privateState.get(extensionId) ?? {}; } diff --git a/src/runtime/extendModVariableContext.ts b/src/runtime/extendModVariableContext.ts index 533ead1c37..37b461941a 100644 --- a/src/runtime/extendModVariableContext.ts +++ b/src/runtime/extendModVariableContext.ts @@ -22,8 +22,8 @@ import apiVersionOptions, { } from "@/runtime/apiVersionOptions"; import { pickBy } from "lodash"; import { type ApiVersion } from "@/types/runtimeTypes"; -import { validateUUID } from "@/types/helpers"; import { assertPlatformCapability } from "@/platform/platformContext"; +import { type Nullishable } from "@/utils/nullishUtils"; /** * Variable for accessing the mod Page State. @@ -95,7 +95,7 @@ export function contextAsPlainObject( * Returns an extended state with a `@mod` variable provided. * @since 1.7.34 * @param originalContext The original context - * @param blueprintId The mod ID, or null if not in a mod + * @param blueprintId The mod ID, if there is one * @param update If true, the mod variable will be updated with the latest state * @param options The runtime version API options */ @@ -106,7 +106,7 @@ function extendModVariableContext( update = false, options, }: { - blueprintId: RegistryId | null; + blueprintId: Nullishable; update?: boolean; options: Pick; }, @@ -135,14 +135,12 @@ function extendModVariableContext( // Eagerly grab the state. It's fast/synchronous since it's in memory in the same JS context. // Previously, we had considered using a proxy to lazily load the state. However, eagerly reading is simpler. // Additionally, in the future to pass the context to the sandbox we'd have to always load the state anyway. - const modState = blueprintId - ? getState({ - namespace: "blueprint", - blueprintId, - // `extensionId` is not used because namespace is `blueprint` - extensionId: validateUUID(null), - }) - : {}; + const modState = getState({ + namespace: "blueprint", + blueprintId, + // `extensionId` is not used because namespace is `blueprint` + extensionId: null, + }); return { ...originalContext, diff --git a/src/runtime/pipelineTests/modVariableContext.test.ts b/src/runtime/pipelineTests/modVariableContext.test.ts index 64297e62d4..f2c3c6ff2f 100644 --- a/src/runtime/pipelineTests/modVariableContext.test.ts +++ b/src/runtime/pipelineTests/modVariableContext.test.ts @@ -36,11 +36,11 @@ beforeEach(() => { describe("modVariableContext", () => { test("use mod variable in variable condition", async () => { setState({ - namespace: "blueprint", + namespace: "blueprint" as const, data: { run: true }, mergeStrategy: "replace", - extensionId: autoUUIDSequence(), - blueprintId: undefined, + extensionId: null, + blueprintId: null, }); const pipeline = [ From ff3aec49adfaf4a44fd16a54d721c54a5a9d3a8c Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 22:35:13 -0400 Subject: [PATCH 21/68] src/bricks/transformers/RunMetadataTransformer.ts --- .../transformers/RunMetadataTransformer.ts | 19 ++++++++++++++++--- src/tsconfig.strictNullChecks.json | 1 + 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/bricks/transformers/RunMetadataTransformer.ts b/src/bricks/transformers/RunMetadataTransformer.ts index 3dbbcd7eb8..5021fd55ed 100644 --- a/src/bricks/transformers/RunMetadataTransformer.ts +++ b/src/bricks/transformers/RunMetadataTransformer.ts @@ -20,6 +20,19 @@ import type { BrickArgs, BrickOptions } from "@/types/runtimeTypes"; import { type Schema, SCHEMA_EMPTY_OBJECT } from "@/types/schemaTypes"; import { validateRegistryId } from "@/types/helpers"; import { propertiesToSchema } from "@/utils/schemaUtils"; +import { type UUID } from "@/types/stringTypes"; + +type ModMetadata = { + id: string; + version?: string; +}; + +type RunMetadata = { + modComponentId: UUID | null; + runId: UUID | null; + mod: ModMetadata | null; + deploymentId: UUID | null; +}; /** * Returns metadata for the current run. @@ -89,9 +102,9 @@ class RunMetadataTransformer extends TransformerABC { ); async transform( - _args: BrickArgs, + _args: BrickArgs, { logger, meta }: BrickOptions, - ): Promise { + ): Promise { const { context } = logger; return { @@ -103,7 +116,7 @@ class RunMetadataTransformer extends TransformerABC { version: context.blueprintVersion, }, deploymentId: context.deploymentId ?? null, - modComponentId: context.extensionId, + modComponentId: context.extensionId ?? null, runId: meta.runId, }; } diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index 82d75fa7d1..1c35c097ac 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -4,6 +4,7 @@ "strictNullChecks": true }, "files": [ + "./bricks/transformers/RunMetadataTransformer.ts", "./bricks/transformers/controlFlow/WithAsyncModVariable.ts", "./bricks/effects/scrollIntoView.ts", "./bricks/effects/runSubTour.ts", From 6c042c615b5e9357ca6777166a5429b19a4a6425 Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 22:43:59 -0400 Subject: [PATCH 22/68] src/bricks/transformers/splitText.ts --- src/bricks/transformers/splitText.ts | 4 ++-- src/tsconfig.strictNullChecks.json | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/bricks/transformers/splitText.ts b/src/bricks/transformers/splitText.ts index e2d3699165..e7a5235131 100644 --- a/src/bricks/transformers/splitText.ts +++ b/src/bricks/transformers/splitText.ts @@ -83,8 +83,8 @@ export class SplitText extends TransformerABC { async transform({ text, - chunkSize, - chunkOverlap, + chunkSize = 1000, + chunkOverlap = 200, }: BrickArgs): Promise { const documents = []; let start = 0; diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index 1c35c097ac..ac07008270 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -4,6 +4,7 @@ "strictNullChecks": true }, "files": [ + "./bricks/transformers/splitText.ts", "./bricks/transformers/RunMetadataTransformer.ts", "./bricks/transformers/controlFlow/WithAsyncModVariable.ts", "./bricks/effects/scrollIntoView.ts", From 751dafecc86d721655534ed805c3fdf7d194a2d1 Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 22:48:23 -0400 Subject: [PATCH 23/68] src/components/fields/schemaFields/widgets/varPopup/useTreeRow.ts --- .../fields/schemaFields/widgets/varPopup/useTreeRow.ts | 4 +++- src/tsconfig.strictNullChecks.json | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/fields/schemaFields/widgets/varPopup/useTreeRow.ts b/src/components/fields/schemaFields/widgets/varPopup/useTreeRow.ts index 713feb79a4..694d2f3534 100644 --- a/src/components/fields/schemaFields/widgets/varPopup/useTreeRow.ts +++ b/src/components/fields/schemaFields/widgets/varPopup/useTreeRow.ts @@ -16,6 +16,7 @@ */ import { type MutableRefObject, useEffect } from "react"; +import { assertNotNullish } from "@/utils/nullishUtils"; /** * A hack to make JSON Tree rows clickable/highlightable. @@ -36,6 +37,7 @@ function useTreeRow({ if (buttonRef.current) { // Find the containing row in the JSONTree const row = buttonRef.current.closest("li"); + assertNotNullish(row, "Could not find row element"); const controller = new AbortController(); row.addEventListener( @@ -66,7 +68,7 @@ function useTreeRow({ if (isActive) { $row.addClass("active"); - $row.get(0).scrollIntoViewIfNeeded?.(); + $row.get(0)?.scrollIntoViewIfNeeded(); } else { $row.removeClass("active"); } diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index ac07008270..69645189a7 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -4,6 +4,7 @@ "strictNullChecks": true }, "files": [ + "./components/fields/schemaFields/widgets/varPopup/useTreeRow.ts", "./bricks/transformers/splitText.ts", "./bricks/transformers/RunMetadataTransformer.ts", "./bricks/transformers/controlFlow/WithAsyncModVariable.ts", From a6c200db5a225a851872b5e7248bb296fe5ba729 Mon Sep 17 00:00:00 2001 From: Ben Loe Date: Mon, 22 Apr 2024 23:04:37 -0400 Subject: [PATCH 24/68] src/components/walkthroughModal/WalkthroughModalApp.tsx --- .../walkthroughModal/WalkthroughModalApp.tsx | 14 +++++++++----- src/tsconfig.strictNullChecks.json | 1 + 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/walkthroughModal/WalkthroughModalApp.tsx b/src/components/walkthroughModal/WalkthroughModalApp.tsx index 3a2046002d..230c1db211 100644 --- a/src/components/walkthroughModal/WalkthroughModalApp.tsx +++ b/src/components/walkthroughModal/WalkthroughModalApp.tsx @@ -35,6 +35,7 @@ import { isMac } from "@/utils/browserUtils"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faEllipsisV } from "@fortawesome/free-solid-svg-icons"; import reportEvent from "@/telemetry/reportEvent"; +import { assertNotNullish } from "@/utils/nullishUtils"; type WalkthroughModalStep = { title: string; @@ -149,15 +150,18 @@ const steps: WalkthroughModalStep[] = [ const WalkthroughModalApp: React.FunctionComponent = () => { const [stepIndex, setStepIndex] = useState(0); const params = new URLSearchParams(location.search); - const opener = JSON.parse(params.get("opener")) as Target; + const openerParam = params.get("opener"); + assertNotNullish(openerParam, "Can't find opener URL parameter"); + const opener = JSON.parse(openerParam) as Target; + // eslint-disable-next-line security/detect-object-injection,@typescript-eslint/no-non-null-assertion,@typescript-eslint/no-unnecessary-type-assertion -- steps are constants defined in this file, and logic will never allow this to go out of bounds + const currentStep = steps[stepIndex]!; useEffect(() => { reportEvent(Events.PAGE_EDITOR_WALKTHROUGH_MODAL_VIEW, { stepNumber: stepIndex + 1, - // eslint-disable-next-line security/detect-object-injection -- steps are constants defined in this file - stepTitle: steps[stepIndex].title, + stepTitle: currentStep.title, }); - }, [stepIndex]); + }, [currentStep.title, stepIndex]); return ( { Step {stepIndex + 1} of {steps.length} - {steps[stepIndex].title} + {currentStep.title} Date: Mon, 22 Apr 2024 23:34:01 -0400 Subject: [PATCH 25/68] src/extensionConsole/pages/brickEditor/BrickHistory.tsx --- src/bricks/effects/assignModVariable.ts | 2 +- src/bricks/effects/pageState.ts | 2 +- .../controlFlow/WithAsyncModVariable.ts | 7 +--- .../pages/brickEditor/BrickHistory.tsx | 34 +++++++++++++------ src/tsconfig.strictNullChecks.json | 1 + 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/bricks/effects/assignModVariable.ts b/src/bricks/effects/assignModVariable.ts index f1c7991e20..bd61016b57 100644 --- a/src/bricks/effects/assignModVariable.ts +++ b/src/bricks/effects/assignModVariable.ts @@ -92,7 +92,7 @@ class AssignModVariable extends EffectABC { }>, { logger }: BrickOptions, ): Promise { - const { blueprintId = null, extensionId = null } = logger.context; + const { blueprintId, extensionId } = logger.context; setState({ namespace: "blueprint", diff --git a/src/bricks/effects/pageState.ts b/src/bricks/effects/pageState.ts index 62cd8cd817..63d54ed364 100644 --- a/src/bricks/effects/pageState.ts +++ b/src/bricks/effects/pageState.ts @@ -141,7 +141,7 @@ export class SetPageState extends TransformerABC { }>, { logger, platform }: BrickOptions, ): Promise { - const { blueprintId = null, extensionId } = logger.context; + const { blueprintId, extensionId } = logger.context; return platform.state.setState({ namespace, diff --git a/src/bricks/transformers/controlFlow/WithAsyncModVariable.ts b/src/bricks/transformers/controlFlow/WithAsyncModVariable.ts index 050b08bd86..023f935f39 100644 --- a/src/bricks/transformers/controlFlow/WithAsyncModVariable.ts +++ b/src/bricks/transformers/controlFlow/WithAsyncModVariable.ts @@ -33,7 +33,6 @@ import { PropError } from "@/errors/businessErrors"; import { type BrickConfig } from "@/bricks/types"; import { castTextLiteralOrThrow } from "@/utils/expressionUtils"; import { propertiesToSchema } from "@/utils/schemaUtils"; -import { assertNotNullish } from "@/utils/nullishUtils"; /** * Map to keep track of the current execution nonce for each Mod Variable. Used to ignore stale request results. @@ -176,11 +175,7 @@ export class WithAsyncModVariable extends TransformerABC { { logger, runPipeline }: BrickOptions, ) { const requestId = uuidv4(); - const { blueprintId = null, extensionId } = logger.context; - assertNotNullish( - extensionId, - "Logger extensionId is null, something went wrong", - ); + const { blueprintId, extensionId } = logger.context; if (isNullOrBlank(stateKey)) { throw new PropError( diff --git a/src/extensionConsole/pages/brickEditor/BrickHistory.tsx b/src/extensionConsole/pages/brickEditor/BrickHistory.tsx index bd2a4f99c0..038c711650 100644 --- a/src/extensionConsole/pages/brickEditor/BrickHistory.tsx +++ b/src/extensionConsole/pages/brickEditor/BrickHistory.tsx @@ -18,7 +18,11 @@ import styles from "./BrickHistory.module.scss"; import React, { Suspense, useEffect, useMemo, useState } from "react"; -import Select, { components, type OptionProps } from "react-select"; +import Select, { + components, + type OptionProps, + type SingleValueProps, +} from "react-select"; import DiffEditor from "@/components/DiffEditor"; import objectHash from "object-hash"; import { type UUID } from "@/types/stringTypes"; @@ -45,7 +49,7 @@ const Content: React.FunctionComponent<{ ); const CustomSingleValue: React.FunctionComponent< - OptionProps + SingleValueProps > = (props) => ( @@ -53,7 +57,7 @@ const CustomSingleValue: React.FunctionComponent< ); const CustomSingleOption: React.FunctionComponent< - OptionProps + OptionProps > = (props) => (