From 52fdfa976243adbcb62e60fe5987047dea235da4 Mon Sep 17 00:00:00 2001 From: Mathias Oterhals Myklebust <24361490+mathiazom@users.noreply.github.com> Date: Thu, 19 Sep 2024 06:17:24 +0000 Subject: [PATCH] v3 - enforce `no-explicit-any` (#662) * refactor: no explicit any enabled eslint rule no-explicit-any and fixed reported errors * feat(RichText): replace custom RichTextBlock with Sanity type --- .eslintrc.cjs | 1 - src/app/(main)/layout.tsx | 2 +- src/app/api/fetchData/route.ts | 2 +- src/blog/components/legal/Legal.tsx | 10 ++- .../components/postPreview/PostPreview.tsx | 16 ++++- src/blog/components/postPreview/mockData.ts | 3 +- src/components/forms/checkbox/Checkbox.tsx | 2 +- src/components/richText/RichText.tsx | 53 ++++---------- src/components/sections/article/mockData.ts | 3 +- src/components/sections/callout/Callout.tsx | 6 +- src/components/sections/callout/mockData.ts | 3 +- src/components/sections/grid/Grid.tsx | 8 +-- src/components/sections/grid/mockdata.ts | 3 +- .../sections/logoSalad/LogoSalad.tsx | 6 +- src/post/lead/Lead.tsx | 8 +-- src/utils/reactNode.ts | 20 ++++++ src/utils/seo.ts | 7 +- studio/components/AnchorSelect.tsx | 12 +--- studio/components/SoMeInputs.tsx | 26 ++++++- studio/components/slug/ReferenceSlugInput.tsx | 70 ------------------- studio/lib/fetchWithToken.ts | 2 +- studio/lib/interfaces/compensations.ts | 2 +- studio/lib/interfaces/legalDocuments.ts | 2 +- studio/lib/interfaces/pages.ts | 2 +- studio/schemas/objects/link.ts | 2 +- studio/utils/validations.ts | 13 ---- 26 files changed, 115 insertions(+), 169 deletions(-) create mode 100644 src/utils/reactNode.ts delete mode 100644 studio/components/slug/ReferenceSlugInput.tsx delete mode 100644 studio/utils/validations.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index a6be710a3..b83f65ddd 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -31,7 +31,6 @@ module.exports = { }, }, rules: { - "@typescript-eslint/no-explicit-any": "off", // TODO "unused-imports/no-unused-imports": "error", "import/no-named-as-default": "off", "import/no-unresolved": "error", diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index 0b6865141..e28cf5ca2 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -18,7 +18,7 @@ import { loadQuery } from "studio/lib/store"; import styles from "./layout.module.css"; -const hasValidData = (data: any) => data && Object.keys(data).length > 0; +const hasValidData = (data: unknown) => data && Object.keys(data).length > 0; export default async function Layout({ children, diff --git a/src/app/api/fetchData/route.ts b/src/app/api/fetchData/route.ts index 88febb8c4..1229e0edd 100644 --- a/src/app/api/fetchData/route.ts +++ b/src/app/api/fetchData/route.ts @@ -8,7 +8,7 @@ const clientWithToken = client.withConfig({ token }); interface FetchRequestBody { query: string; - params?: Record; + params?: Record; } export async function POST(req: Request) { diff --git a/src/blog/components/legal/Legal.tsx b/src/blog/components/legal/Legal.tsx index 69c33620b..c010ffc20 100644 --- a/src/blog/components/legal/Legal.tsx +++ b/src/blog/components/legal/Legal.tsx @@ -1,6 +1,7 @@ import Link from "next/link"; +import { PortableTextBlock } from "sanity"; -import { PortableTextBlock, RichText } from "src/components/richText/RichText"; +import { RichText } from "src/components/richText/RichText"; import Text from "src/components/text/Text"; import { LegalDocument } from "studio/lib/interfaces/legalDocuments"; @@ -9,7 +10,12 @@ import styles from "./legal.module.css"; const extractHeadings = (blocks: PortableTextBlock[]) => { return blocks .filter((block) => block.style === "h2") - .map((block) => block.children?.map((child) => child.text).join(" ") || ""); + .map( + (block) => + (Array.isArray(block.children) && + block.children?.map((child) => child.text).join(" ")) || + "", + ); }; const Legal = ({ document }: { document: LegalDocument }) => { diff --git a/src/blog/components/postPreview/PostPreview.tsx b/src/blog/components/postPreview/PostPreview.tsx index 9b053ad49..37634a7f4 100644 --- a/src/blog/components/postPreview/PostPreview.tsx +++ b/src/blog/components/postPreview/PostPreview.tsx @@ -1,6 +1,8 @@ "use client"; +import { PortableTextBlock } from "sanity"; + import CustomLink from "src/components/link/CustomLink"; -import { PortableTextBlock, RichText } from "src/components/richText/RichText"; +import { RichText } from "src/components/richText/RichText"; import Text from "src/components/text/Text"; import { useConvertSanityImageToNextImage } from "src/utils/hooks/useConvertImage"; import { LinkType } from "studio/lib/interfaces/navigation"; @@ -32,12 +34,20 @@ const PostPreview = ({ }, }; - const truncateFirstBlock = (richText: PortableTextBlock[], limit: number) => { + const truncateFirstBlock = ( + richText: PortableTextBlock[], + limit: number, + ): PortableTextBlock[] => { if (!richText || richText.length === 0) return richText; const firstBlock = richText[0]; let charCount = 0; - const truncatedChildren = firstBlock?.children?.map((child) => { + + if (!("children" in firstBlock) || !Array.isArray(firstBlock.children)) { + return [firstBlock]; + } + + const truncatedChildren = firstBlock.children?.map((child) => { if (charCount >= limit) return { ...child, text: "" }; const remainingChars = limit - charCount; const truncatedText = child.text.slice(0, remainingChars); diff --git a/src/blog/components/postPreview/mockData.ts b/src/blog/components/postPreview/mockData.ts index 7d2f22a69..b9eb9c041 100644 --- a/src/blog/components/postPreview/mockData.ts +++ b/src/blog/components/postPreview/mockData.ts @@ -1,4 +1,5 @@ -import { PortableTextBlock } from "src/components/richText/RichText"; +import { PortableTextBlock } from "sanity"; + import placeholder from "src/stories/assets/image-placeholder.png"; // Common rich text for lead and main content diff --git a/src/components/forms/checkbox/Checkbox.tsx b/src/components/forms/checkbox/Checkbox.tsx index a26d64c79..0c02491c9 100644 --- a/src/components/forms/checkbox/Checkbox.tsx +++ b/src/components/forms/checkbox/Checkbox.tsx @@ -1,6 +1,6 @@ import { PortableText } from "@portabletext/react"; +import { PortableTextBlock } from "sanity"; -import { PortableTextBlock } from "src/components/richText/RichText"; import Text from "src/components/text/Text"; import textStyles from "src/components/text/text.module.css"; diff --git a/src/components/richText/RichText.tsx b/src/components/richText/RichText.tsx index 918d8817a..19f398cf4 100644 --- a/src/components/richText/RichText.tsx +++ b/src/components/richText/RichText.tsx @@ -1,37 +1,18 @@ "use client"; -import { PortableText } from "@portabletext/react"; +import { PortableText, PortableTextReactComponents } from "@portabletext/react"; +import { ReactNode } from "react"; +import { PortableTextBlock } from "sanity"; import Text from "src/components/text/Text"; import textStyles from "src/components/text/text.module.css"; import { useConvertSanityImageToNextImage } from "src/utils/hooks/useConvertImage"; +import { getReactNodeTextContent } from "src/utils/reactNode"; import styles from "./richText.module.css"; -type Children = { - _type: string; - marks: any[]; - text: string; - _key: string; -}; - -export type RichTextType = "h1" | "h2" | "h3" | "normal" | "blockquote"; - -export type PortableTextBlock = { - children?: Children[]; - _type: string; - style?: RichTextType; - _key: string; - asset?: { - _ref: string; - _type: string; - }; - alt?: string; - markDefs?: any[]; -}; - -const formatId = (children: any): string => { - const text = children.join(" "); +const formatId = (children: ReactNode): string => { + const text = getReactNodeTextContent(children); return text .toLowerCase() @@ -44,36 +25,32 @@ const SanityImage = ({ value }: { value: PortableTextBlock }) => { return
{ImageElement}
; }; -const myPortableTextComponents = { +const myPortableTextComponents: Partial = { block: { - h2: ({ children }: any) => ( + h2: ({ children }) => ( {children} ), - h3: ({ children }: any) => ( + h3: ({ children }) => ( {children} ), - normal: ({ children }: any) => {children}, - blockquote: ({ children }: any) => ( + normal: ({ children }) => {children}, + blockquote: ({ children }) => (
{children}
), }, list: { - bullet: ({ children }: any) =>
    {children}
, - number: ({ children }: any) =>
    {children}
, + bullet: ({ children }) =>
    {children}
, + number: ({ children }) =>
    {children}
, }, listItem: { - bullet: ({ children }: any) => ( -
  • {children}
  • - ), - number: ({ children }: any) => ( -
  • {children}
  • - ), + bullet: ({ children }) =>
  • {children}
  • , + number: ({ children }) =>
  • {children}
  • , }, types: { image: SanityImage, diff --git a/src/components/sections/article/mockData.ts b/src/components/sections/article/mockData.ts index 14c719983..136d46aa9 100644 --- a/src/components/sections/article/mockData.ts +++ b/src/components/sections/article/mockData.ts @@ -1,4 +1,5 @@ -import { PortableTextBlock } from "src/components/richText/RichText"; +import { PortableTextBlock } from "sanity"; + import placeholder from "src/stories/assets/image-placeholder.png"; import { LinkType } from "studio/lib/interfaces/navigation"; import { ArticleSection } from "studio/lib/interfaces/pages"; diff --git a/src/components/sections/callout/Callout.tsx b/src/components/sections/callout/Callout.tsx index 8c2f84f14..850660211 100644 --- a/src/components/sections/callout/Callout.tsx +++ b/src/components/sections/callout/Callout.tsx @@ -1,4 +1,4 @@ -import { PortableText } from "@portabletext/react"; +import { PortableText, PortableTextReactComponents } from "@portabletext/react"; import CustomLink from "src/components/link/CustomLink"; import Text from "src/components/text/Text"; @@ -10,8 +10,8 @@ interface CalloutProps { callout: CalloutSection; } -const myPortableTextComponents = { - block: ({ children }: any) => {children}, +const myPortableTextComponents: Partial = { + block: ({ children }) => {children}, }; const Callout = ({ callout }: CalloutProps) => { diff --git a/src/components/sections/callout/mockData.ts b/src/components/sections/callout/mockData.ts index dae8799f5..04e981ae5 100644 --- a/src/components/sections/callout/mockData.ts +++ b/src/components/sections/callout/mockData.ts @@ -1,4 +1,5 @@ -import { PortableTextBlock } from "src/components/richText/RichText"; +import { PortableTextBlock } from "sanity"; + import { LinkType } from "studio/lib/interfaces/navigation"; const RichTextMock: PortableTextBlock[] = [ diff --git a/src/components/sections/grid/Grid.tsx b/src/components/sections/grid/Grid.tsx index c35b28586..6152e86bc 100644 --- a/src/components/sections/grid/Grid.tsx +++ b/src/components/sections/grid/Grid.tsx @@ -1,7 +1,7 @@ "use client"; -import { PortableText } from "@portabletext/react"; +import { PortableText, PortableTextReactComponents } from "@portabletext/react"; +import { PortableTextBlock } from "sanity"; -import { PortableTextBlock } from "src/components/richText/RichText"; import Text from "src/components/text/Text"; import { useConvertSanityImageToNextImage } from "src/utils/hooks/useConvertImage"; import { IImage } from "studio/lib/interfaces/media"; @@ -54,6 +54,6 @@ const Element = ({ ); }; -const myPortableTextComponents = { - block: ({ children }: any) => {children}, +const myPortableTextComponents: Partial = { + block: ({ children }) => {children}, }; diff --git a/src/components/sections/grid/mockdata.ts b/src/components/sections/grid/mockdata.ts index 696c45c01..aad20157c 100644 --- a/src/components/sections/grid/mockdata.ts +++ b/src/components/sections/grid/mockdata.ts @@ -1,4 +1,5 @@ -import { PortableTextBlock } from "src/components/richText/RichText"; +import { PortableTextBlock } from "sanity"; + import placeholder from "src/stories/assets/image-placeholder.png"; const commonRichText: PortableTextBlock[] = [ diff --git a/src/components/sections/logoSalad/LogoSalad.tsx b/src/components/sections/logoSalad/LogoSalad.tsx index 94da98028..639a84884 100644 --- a/src/components/sections/logoSalad/LogoSalad.tsx +++ b/src/components/sections/logoSalad/LogoSalad.tsx @@ -1,4 +1,4 @@ -import { PortableText } from "@portabletext/react"; +import { PortableText, PortableTextReactComponents } from "@portabletext/react"; import Text from "src/components/text/Text"; import { LogoSaladSection } from "studio/lib/interfaces/pages"; @@ -10,8 +10,8 @@ interface LogoSaladProps { logoSalad: LogoSaladSection; } -const myPortableTextComponents = { - block: ({ children }: any) => {children}, +const myPortableTextComponents: Partial = { + block: ({ children }) => {children}, }; export const LogoSalad = ({ logoSalad }: LogoSaladProps) => { diff --git a/src/post/lead/Lead.tsx b/src/post/lead/Lead.tsx index 2e081b64a..fab45b9ef 100644 --- a/src/post/lead/Lead.tsx +++ b/src/post/lead/Lead.tsx @@ -1,15 +1,15 @@ "use client"; -import { PortableText } from "@portabletext/react"; +import { PortableText, PortableTextReactComponents } from "@portabletext/react"; +import { PortableTextBlock } from "sanity"; -import { PortableTextBlock } from "src/components/richText/RichText"; import Text from "src/components/text/Text"; import { useConvertSanityImageToNextImage } from "src/utils/hooks/useConvertImage"; import { IImage } from "studio/lib/interfaces/media"; import styles from "./lead.module.css"; -const myPortableTextComponents = { - block: ({ children }: any) => {children}, +const myPortableTextComponents: Partial = { + block: ({ children }) => {children}, }; const Lead = ({ diff --git a/src/utils/reactNode.ts b/src/utils/reactNode.ts new file mode 100644 index 000000000..f2eef11b7 --- /dev/null +++ b/src/utils/reactNode.ts @@ -0,0 +1,20 @@ +import { ReactNode, isValidElement } from "react"; + +export function getReactNodeTextContent(node: ReactNode): string { + if (typeof node === "string" || typeof node === "number") { + return node.toString(); + } + + if (Array.isArray(node)) { + return node.map(getReactNodeTextContent).join(""); + } + + if (isValidElement(node)) { + const children = node.props.children; + if (children) { + return getReactNodeTextContent(children); + } + } + + return ""; +} diff --git a/src/utils/seo.ts b/src/utils/seo.ts index d1ad7457e..30ab3f9a5 100644 --- a/src/utils/seo.ts +++ b/src/utils/seo.ts @@ -1,7 +1,8 @@ import { toPlainText } from "@portabletext/toolkit"; +import type { QueryParams } from "@sanity/client"; import { Metadata } from "next"; +import { PortableTextBlock } from "sanity"; -import { PortableTextBlock } from "src/components/richText/RichText"; import { urlFor } from "studio/lib/image"; import { BrandAssets } from "studio/lib/interfaces/brandAssets"; import { CompanyInfo } from "studio/lib/interfaces/companyDetails"; @@ -32,7 +33,7 @@ export const OPEN_GRAPH_IMAGE_DIMENSIONS = { export async function fetchSeoData( query: string, - variables?: any, + variables?: QueryParams | undefined, ): Promise { try { const { data } = await loadQuery(query, variables); @@ -45,7 +46,7 @@ export async function fetchSeoData( export async function fetchPostSeoData( query: string, - variables?: any, + variables?: QueryParams | undefined, ): Promise { try { const { data } = await loadQuery(query, variables); diff --git a/studio/components/AnchorSelect.tsx b/studio/components/AnchorSelect.tsx index 1d68f3d97..3d878f266 100644 --- a/studio/components/AnchorSelect.tsx +++ b/studio/components/AnchorSelect.tsx @@ -1,17 +1,9 @@ import { Button, Select, Stack } from "@sanity/ui"; import React, { useEffect, useState } from "react"; -import { PatchEvent, set, unset, useFormValue } from "sanity"; +import { PatchEvent, StringInputProps, set, unset, useFormValue } from "sanity"; import { fetchWithToken } from "studio/lib/fetchWithToken"; -interface AnchorSelectProps { - value?: string; - type: any; - onChange: (event: PatchEvent) => void; - path: any[]; - schemaType: any; -} - interface AnchorItem { basicTitle?: string; value: string; @@ -30,7 +22,7 @@ function fromCamelCase(value?: string) { }); // Capitalize the first letter } -const AnchorSelect = ({ value, onChange, path }: AnchorSelectProps) => { +const AnchorSelect = ({ value, onChange, path }: StringInputProps) => { const [listItems, setListItems] = useState([]); // Extract the internal link reference from the form value diff --git a/studio/components/SoMeInputs.tsx b/studio/components/SoMeInputs.tsx index 322bbbb6c..3707786fe 100644 --- a/studio/components/SoMeInputs.tsx +++ b/studio/components/SoMeInputs.tsx @@ -2,6 +2,8 @@ import { Box, Label, Select, Stack, TextInput } from "@sanity/ui"; import React from "react"; import { ObjectInputProps, set } from "sanity"; +import { SocialMediaLink } from "studio/lib/interfaces/socialMedia"; + export const SoMePlatforms: { [key: string]: string } = { facebook: "Facebook", x: "X", @@ -23,10 +25,28 @@ const detectPlatformFromUrl = (url: string): string | null => { return null; }; -const SoMeInputs: React.FC>> = ({ +function isSocialMediaLinkType(value: unknown): value is SocialMediaLink { + return ( + typeof value === "object" && + value !== null && + "_type" in value && + typeof value._type === "string" && + "url" in value && + typeof value.url === "string" && + "platform" in value && + typeof value.platform === "string" + ); +} + +const SoMeInputs: React.FC>> = ({ value = {}, onChange, }) => { + if (!isSocialMediaLinkType(value)) { + console.error("Unexpected value type for SoMeInputs"); + return; + } + const handleUrlChange = (event: React.ChangeEvent) => { if (!onChange) return; const newUrl = event.target.value; @@ -58,7 +78,7 @@ const SoMeInputs: React.FC>> = ({ @@ -72,7 +92,7 @@ const SoMeInputs: React.FC>> = ({