From eda0763bf657f3c2ae2145373f8224675a555749 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 12:06:02 +0000 Subject: [PATCH 1/3] admin-rte: Enable TypeScript strict mode Strict mode is enabled in the starter, so the packages should follow. All four errors come from strictFunctionTypes, which compares callback parameters contravariantly: - ControlButton attaches onButtonClick to a button element, so the prop is typed with MouseEvent instead of the wider MouseEvent. - useInlineStyleType annotated its click handler with the DOM's global MouseEvent rather than React's, so it did not match IFeatureConfig. - createComponentSlot drops the generic of MUI's Select, which types the slot's onChange with an unknown value. useBlockTypes follows and narrows the block type where it is read. - requiredValidator is no longer declared as FieldValidator: its optional meta parameter makes the type invariant in the field value, so the validator could not be passed to a string-valued Field. --- .changeset/admin-rte-strict-mode.md | 9 +++++++++ .../admin/admin-rte/src/core/Controls/ControlButton.tsx | 2 +- .../admin/admin-rte/src/core/Controls/useBlockTypes.tsx | 8 +++++--- .../admin-rte/src/core/Controls/useInlineStyleType.tsx | 2 +- packages/admin/admin-rte/src/utils/requiredValidator.tsx | 4 ++-- packages/admin/admin-rte/tsconfig.json | 3 ++- 6 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 .changeset/admin-rte-strict-mode.md diff --git a/.changeset/admin-rte-strict-mode.md b/.changeset/admin-rte-strict-mode.md new file mode 100644 index 00000000000..f134eecc69e --- /dev/null +++ b/.changeset/admin-rte-strict-mode.md @@ -0,0 +1,9 @@ +--- +"@dextinity/admin-rte": patch +--- + +Type `requiredValidator` and `ControlButton`'s `onButtonClick` more precisely + +`requiredValidator` is no longer declared as a `FieldValidator`. Its optional `meta` parameter made the type invariant in the field value, which kept the validator from being used for a `string`-valued field. + +`ControlButton` receives its `onButtonClick` handler with a `MouseEvent`, matching the `button` element the handler is attached to. diff --git a/packages/admin/admin-rte/src/core/Controls/ControlButton.tsx b/packages/admin/admin-rte/src/core/Controls/ControlButton.tsx index c9a01d721b1..617143463a7 100644 --- a/packages/admin/admin-rte/src/core/Controls/ControlButton.tsx +++ b/packages/admin/admin-rte/src/core/Controls/ControlButton.tsx @@ -72,7 +72,7 @@ export interface IProps }> { disabled?: boolean; selected?: boolean; - onButtonClick?: (e: MouseEvent) => void; + onButtonClick?: (e: MouseEvent) => void; icon?: ForwardRefExoticComponent & RefAttributes>; /** @deprecated use icon instead */ diff --git a/packages/admin/admin-rte/src/core/Controls/useBlockTypes.tsx b/packages/admin/admin-rte/src/core/Controls/useBlockTypes.tsx index ddc35e420bd..434f6386d9f 100644 --- a/packages/admin/admin-rte/src/core/Controls/useBlockTypes.tsx +++ b/packages/admin/admin-rte/src/core/Controls/useBlockTypes.tsx @@ -44,7 +44,7 @@ const createFeaturesFromBlocktypeMap = })), ]; -type BlockChangeEvent = SelectChangeEvent; +type BlockChangeEvent = SelectChangeEvent; export interface BlockTypesApi { dropdownFeatures: IFeatureConfig[]; @@ -87,13 +87,15 @@ export default function useBlockTypes({ (e: BlockChangeEvent) => { e.preventDefault(); - if (!e.target.value) { + const blockType = typeof e.target.value === "string" ? e.target.value : undefined; + + if (!blockType) { const currentBlock = getCurrentBlock(editorState); if (currentBlock) { setEditorState(RichUtils.toggleBlockType(editorState, currentBlock.getType())); } } else { - setEditorState(RichUtils.toggleBlockType(editorState, e.target.value)); + setEditorState(RichUtils.toggleBlockType(editorState, blockType)); } // keeps editor focused setTimeout(() => { diff --git a/packages/admin/admin-rte/src/core/Controls/useInlineStyleType.tsx b/packages/admin/admin-rte/src/core/Controls/useInlineStyleType.tsx index 436709026d0..68243130106 100644 --- a/packages/admin/admin-rte/src/core/Controls/useInlineStyleType.tsx +++ b/packages/admin/admin-rte/src/core/Controls/useInlineStyleType.tsx @@ -1,7 +1,7 @@ import { RteBold, RteItalic, RteStrikethrough, RteSub, RteSup, RteUnderlined } from "@dextinity/admin-icons"; import * as detectBrowser from "detect-browser"; import { type Editor, type EditorState, RichUtils } from "draft-js"; -import { type RefObject, useCallback, useMemo } from "react"; +import { type MouseEvent, type RefObject, useCallback, useMemo } from "react"; import { FormattedMessage } from "react-intl"; import type { SupportedThings } from "../Rte"; diff --git a/packages/admin/admin-rte/src/utils/requiredValidator.tsx b/packages/admin/admin-rte/src/utils/requiredValidator.tsx index 0a6eee71956..7a2716875a6 100644 --- a/packages/admin/admin-rte/src/utils/requiredValidator.tsx +++ b/packages/admin/admin-rte/src/utils/requiredValidator.tsx @@ -1,10 +1,10 @@ import { convertFromRaw, type RawDraftContentState } from "draft-js"; -import type { FieldValidator } from "final-form"; +import type { ReactNode } from "react"; import { FormattedMessage } from "react-intl"; const requiredMessage = ; -export const requiredValidator: FieldValidator = (value) => { +export const requiredValidator = (value: string | RawDraftContentState | undefined): ReactNode => { if (value === undefined) { return requiredMessage; } diff --git a/packages/admin/admin-rte/tsconfig.json b/packages/admin/admin-rte/tsconfig.json index b27b6630db3..ba4a34821aa 100644 --- a/packages/admin/admin-rte/tsconfig.json +++ b/packages/admin/admin-rte/tsconfig.json @@ -1,7 +1,8 @@ { "compilerOptions": { "outDir": "lib", - "rootDir": "src" + "rootDir": "src", + "strict": true }, "extends": "../tsconfig.base.json", "include": ["./src"] From 207fee84b3e57a89f0cf05a2759adfcf8f8e3eb1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 13:25:52 +0000 Subject: [PATCH 2/3] admin-rte: Address review on the strict mode fixes Keep requiredValidator a FieldValidator by making it generic over the field value, instead of dropping the final-form type. FieldValidator is invariant in the field value because FieldState both accepts and returns it, so a single wide instantiation could not be passed to a string-valued Field; a generic can. Explain in useBlockTypes why the Select slot's event cannot stay typed with DraftBlockType, and why the value has to be narrowed before use. --- .changeset/admin-rte-strict-mode.md | 2 +- .../admin/admin-rte/src/core/Controls/useBlockTypes.tsx | 6 ++++++ packages/admin/admin-rte/src/utils/requiredValidator.tsx | 8 +++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.changeset/admin-rte-strict-mode.md b/.changeset/admin-rte-strict-mode.md index f134eecc69e..f44d7bd515f 100644 --- a/.changeset/admin-rte-strict-mode.md +++ b/.changeset/admin-rte-strict-mode.md @@ -4,6 +4,6 @@ Type `requiredValidator` and `ControlButton`'s `onButtonClick` more precisely -`requiredValidator` is no longer declared as a `FieldValidator`. Its optional `meta` parameter made the type invariant in the field value, which kept the validator from being used for a `string`-valued field. +`requiredValidator` is now generic over the field value. `FieldValidator` is invariant in it, so the previous single instantiation could not be passed to a `string`-valued `Field`. `ControlButton` receives its `onButtonClick` handler with a `MouseEvent`, matching the `button` element the handler is attached to. diff --git a/packages/admin/admin-rte/src/core/Controls/useBlockTypes.tsx b/packages/admin/admin-rte/src/core/Controls/useBlockTypes.tsx index 434f6386d9f..84d4c98d0b4 100644 --- a/packages/admin/admin-rte/src/core/Controls/useBlockTypes.tsx +++ b/packages/admin/admin-rte/src/core/Controls/useBlockTypes.tsx @@ -44,6 +44,9 @@ const createFeaturesFromBlocktypeMap = })), ]; +// Not `SelectChangeEvent`: the `Select` slot is built with `createComponentSlot`, which does not +// forward MUI's generic, so its `onChange` is typed with an `unknown` value. Under `strictFunctionTypes` a handler +// declaring a narrower value is not assignable to it, so the event has to be typed as the slot passes it. type BlockChangeEvent = SelectChangeEvent; export interface BlockTypesApi { @@ -87,6 +90,9 @@ export default function useBlockTypes({ (e: BlockChangeEvent) => { e.preventDefault(); + // `DraftBlockType` is a string, so the `unknown` value has to be narrowed before it can be used as one. + // Every block type `MenuItem` is keyed by its block type, so anything else cannot occur and is treated + // like the empty selection below. const blockType = typeof e.target.value === "string" ? e.target.value : undefined; if (!blockType) { diff --git a/packages/admin/admin-rte/src/utils/requiredValidator.tsx b/packages/admin/admin-rte/src/utils/requiredValidator.tsx index 7a2716875a6..1cda5275433 100644 --- a/packages/admin/admin-rte/src/utils/requiredValidator.tsx +++ b/packages/admin/admin-rte/src/utils/requiredValidator.tsx @@ -1,10 +1,16 @@ import { convertFromRaw, type RawDraftContentState } from "draft-js"; +import type { FieldValidator } from "final-form"; import type { ReactNode } from "react"; import { FormattedMessage } from "react-intl"; const requiredMessage = ; -export const requiredValidator = (value: string | RawDraftContentState | undefined): ReactNode => { +/** + * Generic over the field value because `FieldValidator` is invariant in it: `FieldState` both accepts and returns the + * value, so a single `FieldValidator` cannot be passed to a `string`-valued + * `Field`. As a generic it stays a `FieldValidator` for either field value. + */ +export const requiredValidator = (...[value]: Parameters>): ReactNode => { if (value === undefined) { return requiredMessage; } From 2f4dc9ae441f7c06c0275b395024a728c111342b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 15:24:15 +0000 Subject: [PATCH 3/3] admin-rte: Drop the comments on the strict mode fixes The reasoning belongs in the review conversation, not in the code. --- .../admin/admin-rte/src/core/Controls/useBlockTypes.tsx | 6 ------ packages/admin/admin-rte/src/utils/requiredValidator.tsx | 5 ----- 2 files changed, 11 deletions(-) diff --git a/packages/admin/admin-rte/src/core/Controls/useBlockTypes.tsx b/packages/admin/admin-rte/src/core/Controls/useBlockTypes.tsx index 84d4c98d0b4..434f6386d9f 100644 --- a/packages/admin/admin-rte/src/core/Controls/useBlockTypes.tsx +++ b/packages/admin/admin-rte/src/core/Controls/useBlockTypes.tsx @@ -44,9 +44,6 @@ const createFeaturesFromBlocktypeMap = })), ]; -// Not `SelectChangeEvent`: the `Select` slot is built with `createComponentSlot`, which does not -// forward MUI's generic, so its `onChange` is typed with an `unknown` value. Under `strictFunctionTypes` a handler -// declaring a narrower value is not assignable to it, so the event has to be typed as the slot passes it. type BlockChangeEvent = SelectChangeEvent; export interface BlockTypesApi { @@ -90,9 +87,6 @@ export default function useBlockTypes({ (e: BlockChangeEvent) => { e.preventDefault(); - // `DraftBlockType` is a string, so the `unknown` value has to be narrowed before it can be used as one. - // Every block type `MenuItem` is keyed by its block type, so anything else cannot occur and is treated - // like the empty selection below. const blockType = typeof e.target.value === "string" ? e.target.value : undefined; if (!blockType) { diff --git a/packages/admin/admin-rte/src/utils/requiredValidator.tsx b/packages/admin/admin-rte/src/utils/requiredValidator.tsx index 1cda5275433..7e7ff5f6f24 100644 --- a/packages/admin/admin-rte/src/utils/requiredValidator.tsx +++ b/packages/admin/admin-rte/src/utils/requiredValidator.tsx @@ -5,11 +5,6 @@ import { FormattedMessage } from "react-intl"; const requiredMessage = ; -/** - * Generic over the field value because `FieldValidator` is invariant in it: `FieldState` both accepts and returns the - * value, so a single `FieldValidator` cannot be passed to a `string`-valued - * `Field`. As a generic it stays a `FieldValidator` for either field value. - */ export const requiredValidator = (...[value]: Parameters>): ReactNode => { if (value === undefined) { return requiredMessage;