From 0c482535e194f19dba20bee82a82828a5cdb382a Mon Sep 17 00:00:00 2001 From: Graham Langford <30706330+grahamlangford@users.noreply.github.com> Date: Thu, 1 Aug 2024 08:40:05 -0500 Subject: [PATCH] Strict null checks -- 93.2% (#8961) * JQueryReaderOptions * getAllReaders * auto-add * Tabs.tsx * ApiTaskOptions.tsx * PushOptions.tsx * ActivateModDefinitionPage.tsx * auto-add * DeploymentModal.tsx * auto-add * ListView.tsx * test updates * auto-add * ConvertToModModalBody.tsx * auto-add * CancelPublishContent.tsx * EditPublishContent.tsx * PublishedContent.tsx * PublishModContent.tsx * auto-add * PublishModModals.tsx * auto-add * ModEditorPane.tsx * cleanup type --- src/bricks/readers/getAllReaders.ts | 6 +- .../jquery/JQueryReaderOptions.tsx | 19 ++++-- src/contrib/automationanywhere/RunApiTask.ts | 3 +- src/contrib/zapier/PushOptions.tsx | 2 +- .../pages/activateMod/ActivateModCard.tsx | 2 +- .../activateMod/ActivateModDefinitionPage.tsx | 4 +- .../pages/deployments/DeploymentModal.tsx | 4 +- .../pages/mods/listView/ListView.tsx | 3 + .../ConvertToModModalBody.tsx | 10 +-- .../shareModals/CancelPublishContent.tsx | 20 ++++-- .../modals/shareModals/EditPublishContent.tsx | 7 +- .../modals/shareModals/PublishModContent.tsx | 19 +++++- .../modals/shareModals/PublishModModals.tsx | 6 +- .../modals/shareModals/PublishedContent.tsx | 9 ++- src/pageEditor/panes/ModEditorPane.tsx | 8 ++- src/sidebar/Tabs.tsx | 12 ++-- src/sidebar/sidebarSelectors.ts | 13 ++-- src/store/extensionsSelectors.ts | 7 +- src/store/settings/settingsSelectors.ts | 3 +- src/store/sidebar/eventKeyUtils.test.ts | 8 +-- src/store/sidebar/eventKeyUtils.tsx | 14 ++-- src/store/sidebar/initialState.ts | 1 - src/store/sidebar/sidebarSlice.test.ts | 2 +- src/tsconfig.strictNullChecks.json | 68 ++++++++++++++----- src/types/sidebarTypes.ts | 4 +- src/utils/registryUtils.ts | 10 ++- 26 files changed, 179 insertions(+), 85 deletions(-) diff --git a/src/bricks/readers/getAllReaders.ts b/src/bricks/readers/getAllReaders.ts index 1ac1ff36f..7d52cf23e 100644 --- a/src/bricks/readers/getAllReaders.ts +++ b/src/bricks/readers/getAllReaders.ts @@ -22,8 +22,8 @@ import { ImageReader } from "./ImageReader"; import { SelectionReader } from "./SelectionReader"; import { ImageExifReader } from "./ImageExifReader"; import { ElementReader } from "./ElementReader"; -import { registerFactory } from "./factory"; -import { frameworkReadFactory } from "./frameworkReader"; +import { type Read, registerFactory } from "./factory"; +import { type FrameworkConfig, frameworkReadFactory } from "./frameworkReader"; import { readJQuery } from "@/bricks/readers/jquery"; import { HtmlReader } from "./HtmlReader"; import DocumentReader from "./DocumentReader"; @@ -59,7 +59,7 @@ export function registerReaderFactories(): void { registerFactory("react", frameworkReadFactory("react")); registerFactory("vue", frameworkReadFactory("vue")); registerFactory("vuejs", frameworkReadFactory("vue")); - registerFactory("jquery", readJQuery); + registerFactory("jquery", readJQuery as unknown as Read); } export default getAllReaders; diff --git a/src/bricks/transformers/jquery/JQueryReaderOptions.tsx b/src/bricks/transformers/jquery/JQueryReaderOptions.tsx index ee9ad9fc4..bff212bfd 100644 --- a/src/bricks/transformers/jquery/JQueryReaderOptions.tsx +++ b/src/bricks/transformers/jquery/JQueryReaderOptions.tsx @@ -47,6 +47,8 @@ import { joinName } from "@/utils/formUtils"; import { freshIdentifier } from "@/utils/variableUtils"; import useAsyncEffect from "use-async-effect"; import { inspectedTab } from "@/pageEditor/context/connection"; +import { assertNotNullish } from "@/utils/nullishUtils"; +import { type SetOptional } from "type-fest"; /** * Version of SelectorConfig where fields may be expressions. @@ -182,9 +184,9 @@ const SelectorCard: React.FC<{ */ onChange: (item: SelectorItem) => void; /** - * Delete handler, or null if selector item cannot be deleted. + * Delete handler, or undefined if selector item cannot be deleted. */ - onDelete: (() => void) | null; + onDelete: (() => void) | undefined; /** * The Formik path to selector configuration. */ @@ -225,9 +227,11 @@ const SelectorCard: React.FC<{ return []; }, [selectorDefinition.selector, rootSelector]), - [], + [] as AttributeExample[], ); + assertNotNullish(attributeExamples, "attributeExamples is nullish"); + const typeOption = inferActiveTypeOption(selectorDefinition); const typeOptions = typeOptionsFactory(attributeExamples, typeOption); @@ -313,9 +317,10 @@ const SelectorCard: React.FC<{ selector: produce(selectorDefinition, (draft) => { // `draft` is either a SingleSelector or a ChildrenSelector. Cast as intersection type so we can clean // up the values in the alternative type. - const commonDraft = draft as SingleSelector & ChildrenSelector; + const commonDraft = draft as SingleSelector & + SetOptional; - if (next.startsWith(ATTRIBUTE_OPTION_VALUE_PREFIX)) { + if (next?.startsWith(ATTRIBUTE_OPTION_VALUE_PREFIX)) { const attributeName = next.slice( ATTRIBUTE_OPTION_VALUE_PREFIX.length, ); @@ -376,7 +381,7 @@ const SelectorCard: React.FC<{ const SelectorsOptions: React.FC<{ path: string; - rootSelector: string; + rootSelector: string | null; nestingLevel: number; }> = ({ path, rootSelector, nestingLevel }) => { const configName = partial(joinName, path); @@ -468,7 +473,7 @@ const SelectorsOptions: React.FC<{ selectorItems.filter((_, i) => i !== index), ); } - : null + : undefined } path={configName(name)} /> diff --git a/src/contrib/automationanywhere/RunApiTask.ts b/src/contrib/automationanywhere/RunApiTask.ts index c9ef2e2a4..b2039804e 100644 --- a/src/contrib/automationanywhere/RunApiTask.ts +++ b/src/contrib/automationanywhere/RunApiTask.ts @@ -31,8 +31,9 @@ import { type ApiTaskArgs } from "@/contrib/automationanywhere/aaTypes"; import { type BrickArgs, type BrickOptions } from "@/types/runtimeTypes"; import { BusinessError } from "@/errors/businessErrors"; import { minimalSchemaFactory } from "@/utils/schemaUtils"; +import { type SetRequired } from "type-fest"; -export const RUN_API_TASK_INPUT_SCHEMA: Schema = { +export const RUN_API_TASK_INPUT_SCHEMA: SetRequired = { $schema: "https://json-schema.org/draft/2019-09/schema#", type: "object", properties: { diff --git a/src/contrib/zapier/PushOptions.tsx b/src/contrib/zapier/PushOptions.tsx index 290bd6130..642c9ad3c 100644 --- a/src/contrib/zapier/PushOptions.tsx +++ b/src/contrib/zapier/PushOptions.tsx @@ -55,7 +55,7 @@ function useHooks(): AsyncState { } const ZapField: React.FunctionComponent< - SchemaFieldProps & { hooks: Webhook[]; error: unknown } + SchemaFieldProps & { hooks?: Webhook[]; error: unknown } > = ({ hooks, error, ...props }) => { const options = useMemo( () => diff --git a/src/extensionConsole/pages/activateMod/ActivateModCard.tsx b/src/extensionConsole/pages/activateMod/ActivateModCard.tsx index 117cd5e73..147a1b8da 100644 --- a/src/extensionConsole/pages/activateMod/ActivateModCard.tsx +++ b/src/extensionConsole/pages/activateMod/ActivateModCard.tsx @@ -76,7 +76,7 @@ const WizardHeader: React.VoidFunctionComponent<{ const ActivateModCard: React.FC<{ modDefinition: ModDefinition; isReactivate: boolean; - forceModComponentId: UUID; + forceModComponentId?: UUID; }> = ({ modDefinition, isReactivate, forceModComponentId }) => { const dispatch = useDispatch(); diff --git a/src/extensionConsole/pages/activateMod/ActivateModDefinitionPage.tsx b/src/extensionConsole/pages/activateMod/ActivateModDefinitionPage.tsx index 5487ba012..ee7089b3d 100644 --- a/src/extensionConsole/pages/activateMod/ActivateModDefinitionPage.tsx +++ b/src/extensionConsole/pages/activateMod/ActivateModDefinitionPage.tsx @@ -33,6 +33,7 @@ import { BusinessError } from "@/errors/businessErrors"; import { DefinitionKinds } from "@/types/registryTypes"; import { truncate } from "lodash"; import { type UUID } from "@/types/stringTypes"; +import { assertNotNullish } from "@/utils/nullishUtils"; /** * Effect to automatically redirect the user to the mods screen if the mod is not found. @@ -114,12 +115,13 @@ const ActivateModDefinitionPage: React.FC<{ } const title = `${isReactivate ? "Reactivate" : "Activate"} ${truncate( - modDefinition.metadata.name, + modDefinition?.metadata.name, { length: 15, }, )}`; + assertNotNullish(modDefinitionQuery.data, "modDefinition is nullish"); // Require that bricks have been fetched at least once before showing. Handles new mod activation where the bricks // haven't been completely fetched yet. // XXX: we might also want to enforce a full re-sync of the brick registry to ensure the latest brick diff --git a/src/extensionConsole/pages/deployments/DeploymentModal.tsx b/src/extensionConsole/pages/deployments/DeploymentModal.tsx index 49757d1a0..affeb6839 100644 --- a/src/extensionConsole/pages/deployments/DeploymentModal.tsx +++ b/src/extensionConsole/pages/deployments/DeploymentModal.tsx @@ -58,11 +58,11 @@ function useCurrentTime() { */ export const CountdownTimer: React.FunctionComponent<{ duration: number; - start: number; + start: number | null; onFinish?: () => void; }> = ({ duration, start, onFinish = noop }) => { const now = useCurrentTime(); - const remaining = duration - (now - start); + const remaining = duration - (now - Number(start)); const isExpired = remaining < 0; useEffect(() => { diff --git a/src/extensionConsole/pages/mods/listView/ListView.tsx b/src/extensionConsole/pages/mods/listView/ListView.tsx index 1c25dcd42..442f37d0c 100644 --- a/src/extensionConsole/pages/mods/listView/ListView.tsx +++ b/src/extensionConsole/pages/mods/listView/ListView.tsx @@ -23,6 +23,7 @@ import ListGroupHeader from "@/extensionConsole/pages/mods/listView/ListGroupHea import { uuidv4 } from "@/types/helpers"; import ListItemErrorBoundary from "@/extensionConsole/pages/mods/listView/ListItemErrorBoundary"; import { type ModsPageContentProps } from "@/extensionConsole/pages/mods/modsPageTypes"; +import { assertNotNullish } from "@/utils/nullishUtils"; const ROW_HEIGHT_PX = 90; const HEADER_ROW_HEIGHT_PX = 43; @@ -42,6 +43,7 @@ const ListView: React.VoidFunctionComponent = ({ const getItemSize = useCallback( (index: number) => { const row = expandedRows.at(index); + assertNotNullish(row, `Unable to find row at index ${index}`); return row.isGrouped ? HEADER_ROW_HEIGHT_PX : ROW_HEIGHT_PX; }, [expandedRows], @@ -66,6 +68,7 @@ const ListView: React.VoidFunctionComponent = ({ > {({ index, style }) => { const row = expandedRows.at(index); + assertNotNullish(row, `Unable to find row at index ${index}`); tableInstance.prepareRow(row); return row.isGrouped ? ( diff --git a/src/extensionConsole/pages/mods/modals/convertToModModal/ConvertToModModalBody.tsx b/src/extensionConsole/pages/mods/modals/convertToModModal/ConvertToModModalBody.tsx index 646fe4a5c..766808477 100644 --- a/src/extensionConsole/pages/mods/modals/convertToModModal/ConvertToModModalBody.tsx +++ b/src/extensionConsole/pages/mods/modals/convertToModModal/ConvertToModModalBody.tsx @@ -52,6 +52,7 @@ import { generatePackageId } from "@/utils/registryUtils"; import { FieldDescriptions } from "@/modDefinitions/modDefinitionConstants"; import { pickModDefinitionMetadata } from "@/modDefinitions/util/pickModDefinitionMetadata"; +import { assertNotNullish } from "@/utils/nullishUtils"; type ConvertModFormState = { blueprintId: RegistryId; @@ -125,7 +126,7 @@ const ConvertToModModalBody: React.FunctionComponent = () => { standaloneModDefinitions?.find((x) => x.id === modComponentId); if (modComponent == null) { throw new Error( - `No persisted extension exists with id: ${modComponentId}`, + `No persisted mod component exists with id: ${modComponentId}`, ); } @@ -136,8 +137,8 @@ const ConvertToModModalBody: React.FunctionComponent = () => { const initialValues: ConvertModFormState = useMemo( () => ({ - blueprintId: generatePackageId(scope, modComponent.label), - name: modComponent.label, + blueprintId: generatePackageId(scope, modComponent?.label), + name: modComponent?.label ?? "", version: normalizeSemVerString("1.0.0"), description: "Created with the PixieBrix Page Editor", }), @@ -156,6 +157,7 @@ const ConvertToModModalBody: React.FunctionComponent = () => { helpers: FormikHelpers, ) => { try { + assertNotNullish(modComponent, "modComponent is nullish"); const unsavedModDefinition = mapModComponentToUnsavedModDefinition( modComponent, { @@ -212,7 +214,7 @@ const ConvertToModModalBody: React.FunctionComponent = () => { ); } } catch (error) { - if (isSingleObjectBadRequestError(error) && error.response.data.config) { + if (isSingleObjectBadRequestError(error) && error.response?.data.config) { helpers.setStatus(error.response.data.config); return; } diff --git a/src/extensionConsole/pages/mods/modals/shareModals/CancelPublishContent.tsx b/src/extensionConsole/pages/mods/modals/shareModals/CancelPublishContent.tsx index f9a68b355..92d274c29 100644 --- a/src/extensionConsole/pages/mods/modals/shareModals/CancelPublishContent.tsx +++ b/src/extensionConsole/pages/mods/modals/shareModals/CancelPublishContent.tsx @@ -30,14 +30,16 @@ import { import notify from "@/utils/notify"; import { isSingleObjectBadRequestError } from "@/errors/networkErrorHelpers"; import { getErrorMessage } from "@/errors/errorHelpers"; +import { assertNotNullish } from "@/utils/nullishUtils"; const CancelPublishContent: React.FunctionComponent = () => { const [isCancelling, setCancelling] = React.useState(false); const [error, setError] = React.useState(null); - const { blueprintId } = useSelector(selectShowPublishContext); + const { blueprintId: modId = null } = + useSelector(selectShowPublishContext) ?? {}; const { data: modDefinition, refetch: refetchModDefinitions } = - useOptionalModDefinition(blueprintId); + useOptionalModDefinition(modId); const { data: editablePackages, isFetching: isFetchingEditablePackages } = useGetEditablePackagesQuery(); @@ -53,14 +55,23 @@ const CancelPublishContent: React.FunctionComponent = () => { setError(null); try { + assertNotNullish( + modDefinition, + `modDefinition for modId: ${modId} is nullish`, + ); const newModDefinition = produce(modDefinition, (draft) => { draft.sharing.public = false; }); + assertNotNullish(editablePackages, "editablePackages is nullish"); const packageId = editablePackages.find( (x) => x.name === newModDefinition.metadata.id, )?.id; + assertNotNullish( + packageId, + `packageId for metadata id ${newModDefinition.metadata.id} is nullish`, + ); await updateModDefinition({ packageId, modDefinition: newModDefinition, @@ -73,9 +84,10 @@ const CancelPublishContent: React.FunctionComponent = () => { } catch (error) { if ( isSingleObjectBadRequestError(error) && - error.response.data.config?.length > 0 + Number(error.response?.data.config?.length) > 0 ) { - setError(error.response.data.config.join(" ")); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion -- See if block above + setError(error.response!.data.config!.join(" ")); } else { const message = getErrorMessage(error); setError(message); diff --git a/src/extensionConsole/pages/mods/modals/shareModals/EditPublishContent.tsx b/src/extensionConsole/pages/mods/modals/shareModals/EditPublishContent.tsx index a49cd9a00..1d4277658 100644 --- a/src/extensionConsole/pages/mods/modals/shareModals/EditPublishContent.tsx +++ b/src/extensionConsole/pages/mods/modals/shareModals/EditPublishContent.tsx @@ -24,6 +24,7 @@ import ActivationLink from "@/activation/ActivationLink"; import PublishContentLayout from "./PublishContentLayout"; import { MARKETPLACE_URL } from "@/urlConstants"; +import { assertNotNullish } from "@/utils/nullishUtils"; const EditPublishContent: React.FunctionComponent = () => { const dispatch = useDispatch(); @@ -36,7 +37,9 @@ const EditPublishContent: React.FunctionComponent = () => { dispatch(modModalsSlice.actions.setCancelingPublish()); }; - const { blueprintId } = useSelector(selectShowPublishContext); + const { blueprintId: modId } = useSelector(selectShowPublishContext) ?? {}; + + assertNotNullish(modId, "modId from publish context is nullish"); return ( @@ -57,7 +60,7 @@ const EditPublishContent: React.FunctionComponent = () => {

Public link to share:

- +