From b5d2340daf94e1bc23e4fe9c2e88ff57356b65fe Mon Sep 17 00:00:00 2001
From: adamviktora <84135613+adamviktora@users.noreply.github.com>
Date: Wed, 27 Mar 2024 21:46:23 +0100
Subject: [PATCH] feat(EmptyState): support icons passed directly to
EmptyStateHeader (#612)
* feat(helpers): add getDefaultImports helper
* feat(pfPackageMatches helper): handle imports from react-icons package
* feat(EmptyStateHeader): handle icons passed without EmptyStateIcon wrapper
* refactor(EmptyState icon)
* fix(EmptyState icon): correct undefined check
---
.../src/rules/helpers/getFromPackage.ts | 21 ++++++
.../src/rules/helpers/pfPackageMatches.ts | 4 +-
...tyStateHeader-move-into-emptyState.test.ts | 74 +++++++++++++++++++
.../emptyStateHeader-move-into-emptyState.ts | 73 +++++++++++++++---
4 files changed, 160 insertions(+), 12 deletions(-)
diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/getFromPackage.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/getFromPackage.ts
index 4a435fc6f..05cb308a4 100644
--- a/packages/eslint-plugin-pf-codemods/src/rules/helpers/getFromPackage.ts
+++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/getFromPackage.ts
@@ -5,6 +5,7 @@ import {
Statement,
Directive,
ExportNamedDeclaration,
+ ImportDefaultSpecifier,
ImportSpecifier,
ExportSpecifier,
} from "estree-jsx";
@@ -85,3 +86,23 @@ export function getFromPackage(
return { imports: specifiedImports, exports: specifiedExports };
}
+
+export function getDefaultImportsFromPackage(
+ context: Rule.RuleContext,
+ packageName: string
+): ImportDefaultSpecifier[] {
+ const astBody = context.getSourceCode().ast.body;
+
+ const importDeclarations = astBody.filter(
+ (node) => node?.type === "ImportDeclaration"
+ ) as ImportDeclaration[];
+
+ const importDeclarationsFromPackage = filterByPackageName(
+ importDeclarations,
+ packageName
+ ) as ImportDeclaration[];
+
+ return importDeclarationsFromPackage
+ .filter((imp) => imp.specifiers[0]?.type === "ImportDefaultSpecifier")
+ .map((imp) => imp.specifiers[0]) as ImportDefaultSpecifier[];
+}
diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/pfPackageMatches.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/pfPackageMatches.ts
index e66b66ebf..05c88e392 100644
--- a/packages/eslint-plugin-pf-codemods/src/rules/helpers/pfPackageMatches.ts
+++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/pfPackageMatches.ts
@@ -16,7 +16,9 @@ export function pfPackageMatches(
parts[1] +
"(/dist/(esm|js))?" +
(parts[2] ? "/" + parts[2] : "") +
- "(/(components|helpers)/.*)?$"
+ `(/(components|helpers${
+ parts[1] === "react-icons" ? "|icons" : ""
+ })/.*)?$`
);
return regex.test(nodeSrc);
}
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 974cc63d0..1b400658d 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
@@ -378,5 +378,79 @@ ruleTester.run("emptyStateHeader-move-into-emptyState", rule, {
},
],
},
+ {
+ // with icon prop value not being wrapped in EmptyStateIcon (default import)
+ code: `import {
+ EmptyState,
+ EmptyStateHeader,
+ } from "@patternfly/react-core";
+
+ import CubesIcon from '@patternfly/react-icons/dist/esm/icons/cubes-icon';
+
+ export const EmptyStateHeaderMoveIntoEmptyStateInput = () => (
+
+ }
+ titleText="Empty state"
+ />
+
+ );
+ `,
+ output: `import {
+ EmptyState,
+ EmptyStateHeader,
+ } from "@patternfly/react-core";
+
+ import CubesIcon from '@patternfly/react-icons/dist/esm/icons/cubes-icon';
+
+ 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 icon prop value not being wrapped in EmptyStateIcon (classic import)
+ code: `import {
+ EmptyState,
+ EmptyStateHeader,
+ } from "@patternfly/react-core";
+
+ import { CubesIcon } from '@patternfly/react-icons';
+
+ export const EmptyStateHeaderMoveIntoEmptyStateInput = () => (
+
+ }
+ titleText="Empty state"
+ />
+
+ );
+ `,
+ output: `import {
+ EmptyState,
+ EmptyStateHeader,
+ } from "@patternfly/react-core";
+
+ import { CubesIcon } from '@patternfly/react-icons';
+
+ 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 7ec3c5afa..5af4365a0 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
@@ -5,6 +5,7 @@ import {
getAttributeText,
getAttributeValueText,
getChildElementByName,
+ getDefaultImportsFromPackage,
getExpression,
getFromPackage,
includesImport,
@@ -98,8 +99,52 @@ module.exports = {
const iconPropValue = getExpression(headerIconAttribute?.value);
- const emptyStateIconComponent =
- iconPropValue?.type === "JSXElement" ? iconPropValue : undefined;
+ const iconElementIdentifier =
+ iconPropValue?.type === "JSXElement" &&
+ iconPropValue.openingElement.name.type === "JSXIdentifier"
+ ? iconPropValue.openingElement.name
+ : undefined;
+
+ const iconPropIsEmptyStateIconComponent = () => {
+ const emptyStateIconImport = imports.find(
+ (specifier) => specifier.imported.name === "EmptyStateIcon"
+ );
+
+ if (!emptyStateIconImport) {
+ return false;
+ }
+
+ return (
+ iconElementIdentifier?.name === emptyStateIconImport.local.name
+ );
+ };
+
+ const iconPropIsIconElement = () => {
+ const pfIconsPackage = "@patternfly/react-icons";
+ const { imports: iconSpecifiers } = getFromPackage(
+ context,
+ pfIconsPackage
+ );
+ const iconDefaultSpecifiers = getDefaultImportsFromPackage(
+ context,
+ pfIconsPackage
+ );
+ const allIconSpecifiers = [
+ ...iconSpecifiers,
+ ...iconDefaultSpecifiers,
+ ];
+
+ return (
+ iconElementIdentifier !== undefined &&
+ allIconSpecifiers.some(
+ (spec) => spec.local.name === iconElementIdentifier.name
+ )
+ );
+ };
+
+ const emptyStateIconComponent = iconPropIsEmptyStateIconComponent()
+ ? (iconPropValue as JSXElement)
+ : undefined;
const emptyStateIconComponentIconAttribute =
emptyStateIconComponent &&
@@ -108,23 +153,29 @@ module.exports = {
const emptyStateIconComponentColorAttribute =
emptyStateIconComponent &&
getAttribute(emptyStateIconComponent, "color");
- const emptyStateIconComponentColor = getAttributeText(
- context,
- emptyStateIconComponentColorAttribute
- );
- if (emptyStateIconComponentColor) {
+ if (emptyStateIconComponentColorAttribute) {
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
+ const getIconPropText = () => {
+ if (emptyStateIconComponentIconAttribute) {
+ return context
.getSourceCode()
- .getText(emptyStateIconComponentIconAttribute)
- : "";
+ .getText(emptyStateIconComponentIconAttribute);
+ }
+
+ if (iconPropIsIconElement()) {
+ return `icon={${iconElementIdentifier!.name}}`;
+ }
+
+ return "";
+ };
+
+ const icon = getIconPropText();
context.report({
node,