Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(MenuToggle): remove splitButtonOptions prop and interface #782

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,21 @@ export function getAttributeValue(
return node.value;
}

const isExpressionContainer = valueType === "JSXExpressionContainer";
if (isExpressionContainer && node.expression.type === "Identifier") {
if (valueType !== "JSXExpressionContainer") {
return;
}
Comment on lines +52 to +54
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great cleanup here! 🔥


if (node.expression.type === "Identifier") {
const variableScope = context.getSourceCode().getScope(node);
return getVariableValue(node.expression.name, variableScope);
return getVariableValue(node.expression.name, variableScope, context);
}
if (isExpressionContainer && node.expression.type === "MemberExpression") {
if (node.expression.type === "MemberExpression") {
return getMemberExpression(node.expression);
}
if (isExpressionContainer && node.expression.type === "Literal") {
if (node.expression.type === "Literal") {
return node.expression.value;
}
if (isExpressionContainer && node.expression.type === "ObjectExpression") {
if (node.expression.type === "ObjectExpression") {
return node.expression.properties;
}
}
Expand Down Expand Up @@ -100,7 +103,11 @@ export function getVariableDeclaration(
return undefined;
}

export function getVariableValue(name: string, scope: Scope.Scope | null) {
export function getVariableValue(
name: string,
scope: Scope.Scope | null,
context: Rule.RuleContext
) {
const variableDeclaration = getVariableDeclaration(name, scope);
if (!variableDeclaration) {
return;
Expand All @@ -113,6 +120,13 @@ export function getVariableValue(name: string, scope: Scope.Scope | null) {
if (!variableInit) {
return;
}
if (variableInit.type === "Identifier") {
return getVariableValue(
variableInit.name,
context.getSourceCode().getScope(variableInit),
context
);
}
if (variableInit.type === "Literal") {
return variableInit.value;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
### menuToggle-remove-splitButtonOptions [(#11096)](https://github.com/patternfly/patternfly-react/pull/11096)

We have replaced `splitButtonOptions` prop on MenuToggle with `splitButtonItems`. SplitButtonOptions interface has been deleted, because its `variant` prop no longer supports the "action" option. The `items` prop of SplitButtonOptions will be passed directly to MenuToggle's new `splitButtonItems` prop.

#### Examples

In:

```jsx
%inputExample%
```

Out:

```jsx
%outputExample%
```

Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
const ruleTester = require("../../ruletester");
import * as rule from "./menuToggle-remove-splitButtonOptions";

const message =
"We have replaced `splitButtonOptions` prop on MenuToggle with `splitButtonItems`. SplitButtonOptions interface has been removed, because its `variant` prop no longer supports the 'action' option. The `items` prop of SplitButtonOptions will be passed directly to MenuToggle's new `splitButtonItems` prop.";
const interfaceRemovedMessage = `The SplitButtonOptions interface has been removed.`;

const generalError = {
message,
type: "JSXOpeningElement",
};

ruleTester.run("menuToggle-remove-splitButtonOptions", rule, {
valid: [
{
code: `<MenuToggle splitButtonOptions={{ items: ["Item 1", "Item 2"], variant: "action" }} />`,
},
{
code: `import { MenuToggle } from '@patternfly/react-core'; <MenuToggle someOtherProp />`,
},
],
invalid: [
{
// object expression with "items" property - direct value
code: `import { MenuToggle } from '@patternfly/react-core';
<MenuToggle splitButtonOptions={{ items: ["Item 1", "Item 2"], variant: "action" }} />`,
output: `import { MenuToggle } from '@patternfly/react-core';
<MenuToggle splitButtonItems={["Item 1", "Item 2"]} />`,
errors: [generalError],
},
{
// object expression with "items" property - in a variable
code: `import { MenuToggle } from '@patternfly/react-core';
const sbItems = ["Item 1", "Item 2"];
<MenuToggle splitButtonOptions={{ items: sbItems, variant: "action" }} />`,
output: `import { MenuToggle } from '@patternfly/react-core';
const sbItems = ["Item 1", "Item 2"];
<MenuToggle splitButtonItems={sbItems} />`,
errors: [generalError],
},
{
// identifier
code: `import { MenuToggle } from '@patternfly/react-core'; import { optionsObject } from 'somewhere';
<MenuToggle splitButtonOptions={optionsObject} />`,
output: `import { MenuToggle } from '@patternfly/react-core'; import { optionsObject } from 'somewhere';
<MenuToggle splitButtonItems={optionsObject.items} />`,
errors: [generalError],
},
{
// object expression with a spreaded object
code: `import { MenuToggle } from '@patternfly/react-core'; import { optionsObject } from 'somewhere';
<MenuToggle splitButtonOptions={{ ...optionsObject }} />`,
output: `import { MenuToggle } from '@patternfly/react-core'; import { optionsObject } from 'somewhere';
<MenuToggle splitButtonItems={optionsObject.items} />`,
errors: [generalError],
},
{
// identifier + SplitButtonOptions type
code: `import { MenuToggle, SplitButtonOptions } from '@patternfly/react-core';
const sbOptions: SplitButtonOptions = { items: sbItems, variant: "action" };
<MenuToggle splitButtonOptions={sbOptions} />`,
output: `import { MenuToggle, } from '@patternfly/react-core';
const sbOptions = { items: sbItems, variant: "action" };
<MenuToggle splitButtonItems={sbOptions.items} />`,
errors: [
{
message: interfaceRemovedMessage,
type: "ImportSpecifier",
},
{
message: interfaceRemovedMessage,
type: "Identifier",
},
generalError,
],
},
{
// SplitButtonOptions named export
code: `import { SplitButtonOptions } from '@patternfly/react-core';
export { SplitButtonOptions as SBO };`,
output: `
`,
errors: [
{
message: interfaceRemovedMessage,
type: "ImportSpecifier",
},
{
message: interfaceRemovedMessage,
type: "ExportNamedDeclaration",
},
],
},
{
// SplitButtonOptions default export
code: `import { SplitButtonOptions } from '@patternfly/react-core';
export default SplitButtonOptions;`,
output: `
`,
errors: [
{
message: interfaceRemovedMessage,
type: "ImportSpecifier",
},
{
message: interfaceRemovedMessage,
type: "ExportDefaultDeclaration",
},
],
},
],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { Rule } from "eslint";
import {
ExportDefaultDeclaration,
ExportNamedDeclaration,
Identifier,
ImportSpecifier,
JSXOpeningElement,
Property,
SpreadElement,
} from "estree-jsx";
import {
checkMatchingJSXOpeningElement,
getAttribute,
getFromPackage,
getObjectProperty,
ImportSpecifierWithParent,
removeSpecifierFromDeclaration,
} from "../../helpers";

// https://github.com/patternfly/patternfly-react/pull/11096
module.exports = {
meta: { fixable: "code" },
create: function (context: Rule.RuleContext) {
const message =
"We have replaced `splitButtonOptions` prop on MenuToggle with `splitButtonItems`. SplitButtonOptions interface has been removed, because its `variant` prop no longer supports the 'action' option. The `items` prop of SplitButtonOptions will be passed directly to MenuToggle's new `splitButtonItems` prop.";
const interfaceRemovedMessage = `The SplitButtonOptions interface has been removed.`;

const basePackage = "@patternfly/react-core";
const { imports: menuToggleImports } = getFromPackage(
context,
basePackage,
["MenuToggle"]
);
const { imports: splitButtonOptionsImports } = getFromPackage(
context,
basePackage,
["SplitButtonOptions"]
);
const splitButtonOptionsLocalNames = splitButtonOptionsImports.map(
(specifier) => specifier.local.name
);

if (!menuToggleImports && !splitButtonOptionsImports) {
return;
}

return {
JSXOpeningElement(node: JSXOpeningElement) {
if (!checkMatchingJSXOpeningElement(node, menuToggleImports)) {
return;
}

const splitButtonOptionsProp = getAttribute(node, "splitButtonOptions");

if (
!splitButtonOptionsProp ||
splitButtonOptionsProp.value?.type !== "JSXExpressionContainer"
) {
return;
}

const reportAndFix = (splitButtonItemsValue: string) => {
context.report({
node,
message,
fix(fixer) {
return fixer.replaceText(
splitButtonOptionsProp,
`splitButtonItems={${splitButtonItemsValue}}`
);
},
});
};

const reportAndFixIdentifier = (identifier: Identifier) => {
reportAndFix(`${identifier.name}.items`);
};

const propValue = splitButtonOptionsProp.value.expression;
if (propValue.type === "Identifier") {
reportAndFixIdentifier(propValue);
}

if (propValue.type === "ObjectExpression") {
const properties = propValue.properties.filter(
(prop) => prop.type === "Property"
) as Property[];
const itemsProperty = getObjectProperty(context, properties, "items");

if (itemsProperty) {
const itemsPropertyValueString = context
.getSourceCode()
.getText(itemsProperty.value);

reportAndFix(itemsPropertyValueString);
} else {
const spreadElement = propValue.properties.find(
(prop) => prop.type === "SpreadElement"
) as SpreadElement | undefined;
if (spreadElement && spreadElement.argument.type === "Identifier") {
reportAndFixIdentifier(spreadElement.argument);
}
}
}
},
Identifier(node: Identifier) {
const typeName = (node as any).typeAnnotation?.typeAnnotation?.typeName
?.name;

if (splitButtonOptionsLocalNames.includes(typeName)) {
context.report({
node,
message: interfaceRemovedMessage,
fix(fixer) {
return fixer.remove((node as any).typeAnnotation);
},
});
}
},
ImportSpecifier(node: ImportSpecifier) {
if (splitButtonOptionsImports.includes(node)) {
context.report({
node,
message: interfaceRemovedMessage,
fix(fixer) {
return removeSpecifierFromDeclaration(
fixer,
context,
(node as ImportSpecifierWithParent).parent!,
node
);
},
});
}
},
ExportNamedDeclaration(node: ExportNamedDeclaration) {
const specifierToRemove = node.specifiers.find((specifier) =>
splitButtonOptionsLocalNames.includes(specifier.local.name)
);
if (specifierToRemove) {
context.report({
node,
message: interfaceRemovedMessage,
fix(fixer) {
return removeSpecifierFromDeclaration(
fixer,
context,
node,
specifierToRemove
);
},
});
}
},
ExportDefaultDeclaration(node: ExportDefaultDeclaration) {
if (
node.declaration.type === "Identifier" &&
splitButtonOptionsLocalNames.includes(node.declaration.name)
) {
context.report({
node,
message: interfaceRemovedMessage,
fix(fixer) {
return fixer.remove(node);
},
});
}
},
};
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MenuToggle, SplitButtonOptions } from "@patternfly/react-core";

const sbOptions: SplitButtonOptions = {
items: ["Item 1", "Item 2"],
variant: "action",
};

export const MenuToggleRemoveSplitButtonOptionsInput = () => (
<>
<MenuToggle
splitButtonOptions={{
items: ["Item 1", "Item 2"],
variant: "action",
}}
></MenuToggle>
<MenuToggle splitButtonOptions={sbOptions}></MenuToggle>
</>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { MenuToggle, } from "@patternfly/react-core";

const sbOptions = {
items: ["Item 1", "Item 2"],
variant: "action",
};

export const MenuToggleRemoveSplitButtonOptionsInput = () => (
<>
<MenuToggle
splitButtonItems={["Item 1", "Item 2"]}
></MenuToggle>
<MenuToggle splitButtonItems={sbOptions.items}></MenuToggle>
</>
);
Loading