From b51fa4bc42db6aaf4b0f0de29ffe0b005b76ce32 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Tue, 20 Feb 2024 15:45:02 -0500 Subject: [PATCH 1/5] feat(EmptyState): Add rule to move EmptyStateHeader into EmptyState --- .../emptyStateHeader-move-into-emptyState.md | 18 ++ ...tyStateHeader-move-into-emptyState.test.ts | 221 +++++++++++++++ .../emptyStateHeader-move-into-emptyState.ts | 261 ++++++++++++++++++ ...mptyStateHeaderMoveIntoEmptyStateInput.tsx | 16 ++ ...ptyStateHeaderMoveIntoEmptyStateOutput.tsx | 11 + 5 files changed, 527 insertions(+) create mode 100644 packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.md create mode 100644 packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.test.ts create mode 100644 packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.ts create mode 100644 packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeaderMoveIntoEmptyStateInput.tsx create mode 100644 packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeaderMoveIntoEmptyStateOutput.tsx diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.md b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.md new file mode 100644 index 000000000..adf5f73cb --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.md @@ -0,0 +1,18 @@ +### emptyStateHeader-move-into-emptyState [(#9947)](https://github.com/patternfly/patternfly-react/pull/9947) + +EmptyStateHeader has been moved inside of the EmptyState component and is now only customizable using props, and the EmptyStateIcon component now wraps content passed to the icon prop automatically. Additionally, the titleText prop is now required on EmptyState. + +#### Examples + +In: + +```jsx +%inputExample% +``` + +Out: + +```jsx +%outputExample% +``` + diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.test.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.test.ts new file mode 100644 index 000000000..fe0b9d87c --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.test.ts @@ -0,0 +1,221 @@ +const ruleTester = require("../../ruletester"); +import * as rule from "./emptyStateHeader-move-into-emptyState"; + +ruleTester.run("emptyStateHeader-move-into-emptyState", rule, { + valid: [ + { + code: ``, + }, + { + code: ``, + }, + { + code: `} />`, + }, + { + code: `import { EmptyState } from '@patternfly/react-core'; `, + }, + ], + invalid: [ + { + // with all EmptyStateHeader props + code: `import { + EmptyState, + EmptyStateHeader, + EmptyStateIcon, + CubesIcon + } from "@patternfly/react-core"; + + export const EmptyStateHeaderMoveIntoEmptyStateInput = () => ( + + } + titleClassName="custom-empty-state-title-class" + titleText="Empty state" + /> + + ); + `, + output: `import { + EmptyState, + EmptyStateHeader, + EmptyStateIcon, + CubesIcon + } from "@patternfly/react-core"; + + export const EmptyStateHeaderMoveIntoEmptyStateInput = () => ( + + + ); + `, + errors: [ + { + message: `EmptyStateHeader has been moved inside of the EmptyState component and is now only customizable using props, and the EmptyStateIcon component now wraps content passed to the icon prop automatically. Additionally, the titleText prop is now required on EmptyState.`, + type: "JSXElement", + }, + ], + }, + { + // without any optional EmptyStateHeader props + code: `import { + EmptyState, + EmptyStateHeader, + } from "@patternfly/react-core"; + + export const EmptyStateHeaderMoveIntoEmptyStateInput = () => ( + + + + ); + `, + output: `import { + EmptyState, + EmptyStateHeader, + } from "@patternfly/react-core"; + + export const EmptyStateHeaderMoveIntoEmptyStateInput = () => ( + + + ); + `, + errors: [ + { + message: `EmptyStateHeader has been moved inside of the EmptyState component and is now only customizable using props, and the EmptyStateIcon component now wraps content passed to the icon prop automatically. Additionally, the titleText prop is now required on EmptyState.`, + type: "JSXElement", + }, + ], + }, + { + // without any EmptyStateHeader props or children + code: `import { + EmptyState, + EmptyStateHeader, + } from "@patternfly/react-core"; + + export const EmptyStateHeaderMoveIntoEmptyStateInput = () => ( + + + + ); + `, + output: `import { + EmptyState, + EmptyStateHeader, + } from "@patternfly/react-core"; + + export const EmptyStateHeaderMoveIntoEmptyStateInput = () => ( + + + + ); + `, + errors: [ + { + message: `EmptyStateHeader has been moved inside of the EmptyState component and is now only customizable using props, and the EmptyStateIcon component now wraps content passed to the icon prop automatically. Additionally, the titleText prop is now required on EmptyState. You must manually supply a titleText prop, then you can rerun this codemod.`, + type: "JSXElement", + }, + ], + }, + { + // without any EmptyStateHeader props but with children + code: `import { + EmptyState, + EmptyStateHeader, + } from "@patternfly/react-core"; + + export const EmptyStateHeaderMoveIntoEmptyStateInput = () => ( + + Foo bar + + ); + `, + output: `import { + EmptyState, + EmptyStateHeader, + } from "@patternfly/react-core"; + + export const EmptyStateHeaderMoveIntoEmptyStateInput = () => ( + + + ); + `, + errors: [ + { + message: `EmptyStateHeader has been moved inside of the EmptyState component and is now only customizable using props, and the EmptyStateIcon component now wraps content passed to the icon prop automatically. Additionally, the titleText prop is now required on EmptyState.`, + type: "JSXElement", + }, + ], + }, + { + // with both titleText and children + code: `import { + EmptyState, + EmptyStateHeader, + } from "@patternfly/react-core"; + + export const EmptyStateHeaderMoveIntoEmptyStateInput = () => ( + + Foo + + ); + `, + output: `import { + EmptyState, + EmptyStateHeader, + } from "@patternfly/react-core"; + + export const EmptyStateHeaderMoveIntoEmptyStateInput = () => ( + + Foo + + ); + `, + errors: [ + { + message: `EmptyStateHeader has been moved inside of the EmptyState component and is now only customizable using props, and the EmptyStateIcon component now wraps content passed to the icon prop automatically. Additionally, the titleText prop is now required on EmptyState. Because the children for EmptyStateHeader are now inaccessible you must remove either the children or the titleText prop, then you can rerun this codemod.`, + type: "JSXElement", + }, + ], + }, + { + // with the color prop on the EmptyStateIcon + code: `import { + EmptyState, + EmptyStateHeader, + EmptyStateIcon, + CubesIcon + } from "@patternfly/react-core"; + + export const EmptyStateHeaderMoveIntoEmptyStateInput = () => ( + + } /> + + ); + `, + output: `import { + EmptyState, + EmptyStateHeader, + EmptyStateIcon, + CubesIcon + } from "@patternfly/react-core"; + + export const EmptyStateHeaderMoveIntoEmptyStateInput = () => ( + + + ); + `, + errors: [ + { + message: `The color prop on EmptyStateIcon has been removed. We suggest using the new status prop on EmptyState to apply colors to the icon.`, + type: "JSXElement", + }, + { + message: `EmptyStateHeader has been moved inside of the EmptyState component and is now only customizable using props, and the EmptyStateIcon component now wraps content passed to the icon prop automatically. Additionally, the titleText prop is now required on EmptyState.`, + type: "JSXElement", + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.ts new file mode 100644 index 000000000..a5b415fe7 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.ts @@ -0,0 +1,261 @@ +import { AST, Rule } from "eslint"; +import { + JSXElement, + ImportDeclaration, + ImportSpecifier, + JSXAttribute, + Literal, + Node, + Expression, + ClassExpression, + JSXText, + JSXExpressionContainer, + JSXSpreadChild, + JSXFragment, +} from "estree-jsx"; +import { getFromPackage, pfPackageMatches } from "../../helpers"; + +const baseMessage = + "EmptyStateHeader has been moved inside of the EmptyState component and is now only customizable using props, and the EmptyStateIcon component now wraps content passed to the icon prop automatically. Additionally, the titleText prop is now required on EmptyState."; + +// https://github.com/patternfly/patternfly-react/pull/9947 +module.exports = { + meta: { fixable: "code" }, + create: function (context: Rule.RuleContext) { + const pkg = "@patternfly/react-core"; + const { imports } = getFromPackage(context, pkg); + + function allOfType(nodes: Node[], type: string) { + return nodes.every((specifier) => specifier.type === type); + } + + const includesImport = ( + arr: ImportDeclaration["specifiers"], + targetImport: string + ) => + arr.some( + (specifier) => + allOfType(arr, "ImportSpecifier") && + (specifier as ImportSpecifier).imported.name === targetImport + ); + + if ( + !includesImport(imports, "EmptyStateHeader") || + !includesImport(imports, "EmptyState") + ) { + return {}; + } + + const getChildElementByName = (name: string, node: JSXElement) => + node.children?.find( + (child) => + child.type === "JSXElement" && + child.openingElement.name.type === "JSXIdentifier" && + child.openingElement.name.name === name + ); + + const isComponentNode = (node: JSXElement, componentName: string) => { + if (node.openingElement.name.type === "JSXIdentifier") { + return node.openingElement.name.name === componentName; + } + + return false; + }; + + const getAttribute = ( + node: JSXElement, + attributeName: string + ): JSXAttribute | undefined => { + const attributes = node.openingElement.attributes.filter( + (attr) => attr.type === "JSXAttribute" + ) as JSXAttribute[]; + return attributes.find((attr) => attr.name.name === attributeName); + }; + + const getExpressionValue = (node?: JSXAttribute["value"]) => { + if (!node) { + return; + } + + if (node.type === "JSXExpressionContainer") { + return node.expression; + } + }; + + const getAttributeText = (attribute?: JSXAttribute) => { + if (!attribute) { + return ""; + } + + return context.getSourceCode().getText(attribute); + }; + + const getAttributeValueText = (attribute?: JSXAttribute) => { + if (!attribute || !attribute.value) { + return ""; + } + + const { value } = attribute; + + if (value.type === "Literal") { + return value.value; + } + + return ""; + }; + + const getNodesText = (nodes: Node[]) => { + return nodes + .map((node) => context.getSourceCode().getText(node)) + .join(""); + }; + + const getElementChildText = (children: JSXElement["children"]) => { + if (!children.length) { + return ""; + } + + switch (children[0].type) { + case "JSXText": + return (children as JSXText[]).map((child) => child.value).join(""); + case "JSXExpressionContainer": + case "JSXSpreadChild": + return getNodesText( + children.map( + (child) => + (child as JSXExpressionContainer | JSXSpreadChild).expression + ) + ); + case "JSXElement": + case "JSXFragment": + return getNodesText(children as JSXElement[] | JSXFragment[]); + default: + return ""; + } + }; + + return { + ImportDeclaration(node: ImportDeclaration) { + if ( + !pfPackageMatches(pkg, node.source.value) || + !includesImport(node.specifiers, "EmptyStateHeader") + ) { + return; + } + }, + JSXElement(node: JSXElement) { + if (!isComponentNode(node, "EmptyState")) { + return; + } + + const header = getChildElementByName("EmptyStateHeader", node); + + if (!header || header.type !== "JSXElement") { + return; + } + + const headingClassNameAttribute = getAttribute(header, "className"); + const headingLevelAttribute = getAttribute(header, "headingLevel"); + const titleClassNameAttribute = getAttribute(header, "titleClassName"); + const titleTextAttribute = getAttribute(header, "titleText"); + const headerIconAttribute = getAttribute(header, "icon"); + + const headerChildren = header.children; + + if (!titleTextAttribute && !headerChildren.length) { + context.report({ + node, + message: `${baseMessage} You must manually supply a titleText prop, then you can rerun this codemod.`, + }); + return; + } + + if (titleTextAttribute && headerChildren.length) { + context.report({ + node, + message: `${baseMessage} Because the children for EmptyStateHeader are now inaccessible you must remove either the children or the titleText prop, then you can rerun this codemod.`, + }); + return; + } + + const headingClassNameValue = getAttributeValueText( + headingClassNameAttribute + ); + + const headingClassName = headingClassNameValue + ? `headerClassName="${headingClassNameValue}"` + : ""; + const headingLevel = getAttributeText(headingLevelAttribute); + const titleClassName = getAttributeText(titleClassNameAttribute); + const titleTextPropValue = getAttributeText(titleTextAttribute); + + const titleText = + titleTextPropValue || + `titleText="${getElementChildText(headerChildren)}"`; + + const iconPropValue = getExpressionValue(headerIconAttribute?.value); + + const emptyStateIconComponent = + iconPropValue?.type === "JSXElement" ? iconPropValue : undefined; + + const emptyStateIconComponentIconAttribute = + emptyStateIconComponent && + getAttribute(emptyStateIconComponent, "icon"); + + const emptyStateIconComponentColorAttribute = emptyStateIconComponent && getAttribute(emptyStateIconComponent, "color"); + const emptyStateIconComponentColor = getAttributeText(emptyStateIconComponentColorAttribute); + + if (emptyStateIconComponentColor) { + context.report({ + node, + message: `The color prop on EmptyStateIcon has been removed. We suggest using the new status prop on EmptyState to apply colors to the icon.`, + }); + } + + const icon = emptyStateIconComponentIconAttribute + ? context + .getSourceCode() + .getText(emptyStateIconComponentIconAttribute) + : ""; + + context.report({ + node, + message: baseMessage, + fix(fixer) { + const removeEmptyLineAfter = ( + node?: JSXElement | ImportSpecifier | AST.Token | null + ) => { + if (!node) { + return []; + } + const token = context.getSourceCode().getTokenAfter(node); + + return token && + token.type === "JSXText" && + token.value.trim() === "" + ? [fixer.remove(token)] + : []; + }; + + const removeElement = (node?: JSXElement) => { + if (!node) { + return []; + } + + return [fixer.remove(node)]; + }; + + return [ + fixer.insertTextAfter( + node.openingElement.name, + ` ${headingClassName} ${headingLevel} ${icon} ${titleClassName} ${titleText}` + ), + ...removeElement(header), + ...removeEmptyLineAfter(header), + ]; + }, + }); + }, + }; + }, +}; diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeaderMoveIntoEmptyStateInput.tsx b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeaderMoveIntoEmptyStateInput.tsx new file mode 100644 index 000000000..e3fe33eca --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeaderMoveIntoEmptyStateInput.tsx @@ -0,0 +1,16 @@ +import { + EmptyState, + EmptyStateHeader, + EmptyStateIcon, + CubesIcon +} from "@patternfly/react-core"; + +export const EmptyStateHeaderMoveIntoEmptyStateInput = () => ( + + } + /> + +); diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeaderMoveIntoEmptyStateOutput.tsx b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeaderMoveIntoEmptyStateOutput.tsx new file mode 100644 index 000000000..2772835bc --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeaderMoveIntoEmptyStateOutput.tsx @@ -0,0 +1,11 @@ +import { + EmptyState, + EmptyStateHeader, + EmptyStateIcon, + CubesIcon +} from "@patternfly/react-core"; + +export const EmptyStateHeaderMoveIntoEmptyStateInput = () => ( + + +); From 016ba540967a51fa7850c8f42c6a7dd6967482e2 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Tue, 27 Feb 2024 12:28:12 -0500 Subject: [PATCH 2/5] chore(EmptyState): Address PR feedback --- .../emptyStateHeader-move-into-emptyState.md | 4 +- ...tyStateHeader-move-into-emptyState.test.ts | 25 +++++++++ .../emptyStateHeader-move-into-emptyState.ts | 54 ++++++++++--------- 3 files changed, 58 insertions(+), 25 deletions(-) diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.md b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.md index adf5f73cb..95d61eae7 100644 --- a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.md +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.md @@ -1,6 +1,8 @@ ### emptyStateHeader-move-into-emptyState [(#9947)](https://github.com/patternfly/patternfly-react/pull/9947) -EmptyStateHeader has been moved inside of the EmptyState component and is now only customizable using props, and the EmptyStateIcon component now wraps content passed to the icon prop automatically. Additionally, the titleText prop is now required on EmptyState. +EmptyStateHeader and EmptyStateIcon are now rendered internally within EmptyState and should only be customized using props. Content passed to the `icon` prop on EmptyState will also be wrapped by EmptyStateIcon automatically. + +Additionally, the `titleText` prop is now required on EmptyState. #### Examples diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.test.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.test.ts index fe0b9d87c..eb4c7ea06 100644 --- a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.test.ts +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.test.ts @@ -148,6 +148,31 @@ ruleTester.run("emptyStateHeader-move-into-emptyState", rule, { }, ], }, + { + // without an EmptyStateHeader or titleText + code: `import { EmptyState } from "@patternfly/react-core"; + + export const EmptyStateHeaderMoveIntoEmptyStateInput = () => ( + + Foo bar + + ); + `, + output: `import { EmptyState } from "@patternfly/react-core"; + + export const EmptyStateHeaderMoveIntoEmptyStateInput = () => ( + + Foo bar + + ); + `, + errors: [ + { + message: `EmptyStateHeader has been moved inside of the EmptyState component and is now only customizable using props, and the EmptyStateIcon component now wraps content passed to the icon prop automatically. Additionally, the titleText prop is now required on EmptyState. You must manually supply a titleText prop.`, + type: "JSXElement", + }, + ], + }, { // with both titleText and children code: `import { diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.ts index a5b415fe7..6099f7f9c 100644 --- a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.ts +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.ts @@ -4,10 +4,7 @@ import { ImportDeclaration, ImportSpecifier, JSXAttribute, - Literal, Node, - Expression, - ClassExpression, JSXText, JSXExpressionContainer, JSXSpreadChild, @@ -25,24 +22,24 @@ module.exports = { const pkg = "@patternfly/react-core"; const { imports } = getFromPackage(context, pkg); - function allOfType(nodes: Node[], type: string) { - return nodes.every((specifier) => specifier.type === type); - } + const allOfType = (nodes: Node[], type: string) => + nodes.every((specifier) => specifier.type === type); const includesImport = ( arr: ImportDeclaration["specifiers"], targetImport: string - ) => - arr.some( + ) => { + if (!allOfType(arr, "ImportSpecifier")) { + return false; + } + + return arr.some( (specifier) => - allOfType(arr, "ImportSpecifier") && (specifier as ImportSpecifier).imported.name === targetImport ); + }; - if ( - !includesImport(imports, "EmptyStateHeader") || - !includesImport(imports, "EmptyState") - ) { + if (!includesImport(imports, "EmptyState")) { return {}; } @@ -101,6 +98,10 @@ module.exports = { return value.value; } + if (value.type === "JSXExpressionContainer") { + return context.getSourceCode().getText(value.expression); + } + return ""; }; @@ -135,20 +136,21 @@ module.exports = { }; return { - ImportDeclaration(node: ImportDeclaration) { - if ( - !pfPackageMatches(pkg, node.source.value) || - !includesImport(node.specifiers, "EmptyStateHeader") - ) { - return; - } - }, JSXElement(node: JSXElement) { if (!isComponentNode(node, "EmptyState")) { return; } const header = getChildElementByName("EmptyStateHeader", node); + const emptyStateTitleTextAttribute = getAttribute(node, "titleText"); + + if (!header && !emptyStateTitleTextAttribute) { + context.report({ + node, + message: `${baseMessage} You must manually supply a titleText prop to EmptyState.`, + }); + return; + } if (!header || header.type !== "JSXElement") { return; @@ -165,7 +167,7 @@ module.exports = { if (!titleTextAttribute && !headerChildren.length) { context.report({ node, - message: `${baseMessage} You must manually supply a titleText prop, then you can rerun this codemod.`, + message: `${baseMessage} You must manually supply a titleText prop to EmptyState, then you can rerun this codemod.`, }); return; } @@ -202,8 +204,12 @@ module.exports = { emptyStateIconComponent && getAttribute(emptyStateIconComponent, "icon"); - const emptyStateIconComponentColorAttribute = emptyStateIconComponent && getAttribute(emptyStateIconComponent, "color"); - const emptyStateIconComponentColor = getAttributeText(emptyStateIconComponentColorAttribute); + const emptyStateIconComponentColorAttribute = + emptyStateIconComponent && + getAttribute(emptyStateIconComponent, "color"); + const emptyStateIconComponentColor = getAttributeText( + emptyStateIconComponentColorAttribute + ); if (emptyStateIconComponentColor) { context.report({ From 1658c799be537b3dbf6bb35b170f7e59fb2647ac Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Tue, 27 Feb 2024 12:33:15 -0500 Subject: [PATCH 3/5] chore(EmptyState): Update test verbiage to reflect changes to rule --- .../emptyStateHeader-move-into-emptyState.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.test.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.test.ts index eb4c7ea06..a92581a34 100644 --- a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.test.ts +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.test.ts @@ -113,7 +113,7 @@ ruleTester.run("emptyStateHeader-move-into-emptyState", rule, { `, errors: [ { - message: `EmptyStateHeader has been moved inside of the EmptyState component and is now only customizable using props, and the EmptyStateIcon component now wraps content passed to the icon prop automatically. Additionally, the titleText prop is now required on EmptyState. You must manually supply a titleText prop, then you can rerun this codemod.`, + message: `EmptyStateHeader has been moved inside of the EmptyState component and is now only customizable using props, and the EmptyStateIcon component now wraps content passed to the icon prop automatically. Additionally, the titleText prop is now required on EmptyState. You must manually supply a titleText prop to EmptyState, then you can rerun this codemod.`, type: "JSXElement", }, ], @@ -168,7 +168,7 @@ ruleTester.run("emptyStateHeader-move-into-emptyState", rule, { `, errors: [ { - message: `EmptyStateHeader has been moved inside of the EmptyState component and is now only customizable using props, and the EmptyStateIcon component now wraps content passed to the icon prop automatically. Additionally, the titleText prop is now required on EmptyState. You must manually supply a titleText prop.`, + message: `EmptyStateHeader has been moved inside of the EmptyState component and is now only customizable using props, and the EmptyStateIcon component now wraps content passed to the icon prop automatically. Additionally, the titleText prop is now required on EmptyState. You must manually supply a titleText prop to EmptyState.`, type: "JSXElement", }, ], From 748b4ed48821ff63e5039800d6a6104312dd9210 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Wed, 28 Feb 2024 09:20:11 -0500 Subject: [PATCH 4/5] fix(EmptyState): Fix passing a string in braces for headerClassName --- ...tyStateHeader-move-into-emptyState.test.ts | 30 +++++++++++++++++++ .../emptyStateHeader-move-into-emptyState.ts | 12 +++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.test.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.test.ts index a92581a34..8591677a1 100644 --- a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.test.ts +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.test.ts @@ -242,5 +242,35 @@ ruleTester.run("emptyStateHeader-move-into-emptyState", rule, { }, ], }, + { + // with a string expression for the heading className prop + code: `import { + EmptyState, + EmptyStateHeader, + } from "@patternfly/react-core"; + + export const EmptyStateHeaderMoveIntoEmptyStateInput = () => ( + + + + ); + `, + output: `import { + EmptyState, + EmptyStateHeader, + } from "@patternfly/react-core"; + + export const EmptyStateHeaderMoveIntoEmptyStateInput = () => ( + + + ); + `, + errors: [ + { + message: `EmptyStateHeader has been moved inside of the EmptyState component and is now only customizable using props, and the EmptyStateIcon component now wraps content passed to the icon prop automatically. Additionally, the titleText prop is now required on EmptyState.`, + type: "JSXElement", + }, + ], + }, ], }); diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.ts index 6099f7f9c..7399cd576 100644 --- a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.ts +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.ts @@ -88,6 +88,7 @@ module.exports = { }; const getAttributeValueText = (attribute?: JSXAttribute) => { + console.log("attribute", attribute); if (!attribute || !attribute.value) { return ""; } @@ -95,11 +96,14 @@ module.exports = { const { value } = attribute; if (value.type === "Literal") { - return value.value; + return `"${value.value}"`; } - if (value.type === "JSXExpressionContainer") { - return context.getSourceCode().getText(value.expression); + if ( + value.type === "JSXExpressionContainer" && + value.expression.type === "Literal" + ) { + return `{"${value.expression.value}"}`; } return ""; @@ -185,7 +189,7 @@ module.exports = { ); const headingClassName = headingClassNameValue - ? `headerClassName="${headingClassNameValue}"` + ? `headerClassName=${headingClassNameValue}` : ""; const headingLevel = getAttributeText(headingLevelAttribute); const titleClassName = getAttributeText(titleClassNameAttribute); From d518994c675aaffee4ac5f258300d8a049301258 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Wed, 28 Feb 2024 09:36:15 -0500 Subject: [PATCH 5/5] fix(EmptyState): Support more headerClassName value types --- .../emptyStateHeader-move-into-emptyState.ts | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.ts index 7399cd576..3c8e8e699 100644 --- a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.ts +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateHeaderMoveIntoEmptyState/emptyStateHeader-move-into-emptyState.ts @@ -88,25 +88,11 @@ module.exports = { }; const getAttributeValueText = (attribute?: JSXAttribute) => { - console.log("attribute", attribute); if (!attribute || !attribute.value) { return ""; } - const { value } = attribute; - - if (value.type === "Literal") { - return `"${value.value}"`; - } - - if ( - value.type === "JSXExpressionContainer" && - value.expression.type === "Literal" - ) { - return `{"${value.expression.value}"}`; - } - - return ""; + return context.getSourceCode().getText(attribute.value); }; const getNodesText = (nodes: Node[]) => {