Skip to content

Commit

Permalink
Add refactor-breakpointmods rule (#72)
Browse files Browse the repository at this point in the history
* setup

* added Page rule

* added show rule for ToolbarToggleGroup

* build out all rules

* finished rules, added tests

* PR feedback
  • Loading branch information
evwilkin authored Jun 3, 2020
1 parent 1f35a0c commit e2a02e3
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/eslint-plugin-pf-codemods/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const rules = {
"rename-toolbar-components": require('./lib/rules/rename-toolbar-components'),
"page-header-prop-rename": require('./lib/rules/page-header-prop-rename'),
"page-header-move-avatar": require('./lib/rules/page-header-move-avatar'),
"refactor-breakpointmods": require('./lib/rules/refactor-breakpointmods'),
};

module.exports = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
const { getPackageImports } = require('../helpers');
const componentsToUpdate = [
'Flex',
'FlexItem',
'PageSection',
'ToolbarItem',
'ToolbarToggleGroup'
];
const camelCase = str => str.replace(/-([a-z])/g, groups => groups[1].toUpperCase());
const breakpointPropNames = {
visibility: ['hidden', 'visible'],
widths: ['width_25', 'width_33', 'width_50', 'width_66', 'width_75', 'width_100'],
padding: ['padding', 'noPadding'],
inset: ['insetNone', 'insetSm', 'insetMd', 'insetLg', 'insetXl', 'inset2xl'],
spacer: ['spacerNone', 'spacerXs', 'spacerSm', 'spacerMd', 'spacerLg', 'spacerXl', 'spacer2xl', 'spacer3xl', 'spacer4xl'],
spaceItems: ['spaceItemsNone', 'spaceItemsXs', 'spaceItemsSm', 'spaceItemsMd', 'spaceItemsLg', 'spaceItemsXl', 'spaceItems2xl', 'spaceItems3xl', 'spaceItems4xl'],
show: ['show'],
grow: ['grow'],
shrink: ['shrink'],
flex: ['flexDefault', 'flexNone', 'flex_1', 'flex_2', 'flex_3', 'flex_4'],
direction: ['column', 'columnReverse', 'row', 'rowReverse'],
alignItems: ['alignItemsFlexStart', 'alignItemsFlexEnd', 'alignItemsCenter', 'alignItemsStretch', 'alignItemsBaseline'],
alignContent: ['alignContentFlexStart', 'alignContentFlexEnd', 'alignContentCenter', 'alignContentStretch', 'alignContentSpaceBetween', 'alignContentSpaceAround'],
alignSelf: ['alignSelfFlexStart', 'alignSelfFlexEnd', 'alignSelfCenter', 'alignSelfStretch', 'alignSelfBaseline'],
align: ['alignLeft', 'alignRight'],
justifyContent: ['justifyContentFlexStart', 'justifyContentFlexEnd', 'justifyContentCenter', 'justifyContentSpaceBetween', 'justifyContentSpaceAround', 'justifyContentSpaceEvenly'],
display: ['inlineFlex'],
fullWidth: ['fullWidth'],
flexWrap: ['wrap', 'wrapReverse', 'nowrap']
};
const errorMessages = {
Flex: 'Removed breakpointMods prop from Flex in favor of spacer, spaceItems, grow, shrink, flex, direction, alignItems, alignContent, alignSelf, align, justifyContent, display, fullWidth and flexWrap',
FlexItem: 'Removed breakpointMods prop from FlexItem in favor of spacer, grow, shrink, flex, alignSelf, align, and fullWidth',
ToolbarItem: 'Removed breakpointMods prop from ToolbarItem in favor of visiblity, alignment, and spacer',
ToolbarToggleGroup: 'Removed breakpointMods prop from ToolbarToggleGroup in favor of visiblity, alignment, spacer, and spaceItems'
};

// https://github.com/patternfly/patternfly-react/pull/4310
module.exports = {
create: function(context) {

const imports = getPackageImports(context, '@patternfly/react-core')
.filter(specifier => componentsToUpdate.includes(specifier.imported.name));

return imports.length == 0 ? {} : {
JSXOpeningElement(node) {
const nodeName = node.name.name;
const nodeImport = imports.find(imp => imp.local.name === nodeName);
if (nodeImport) {
const importName = nodeImport.imported.name;
// PageSection
if (importName === 'PageSection') {
const attribute = node.attributes.find(node => node.name && node.name.name === 'hasNoPadding');
if (attribute) {
context.report({
node,
message: `hasNoPadding prop on PageSection component removed in favor of padding={{ default: 'noPadding' }}`,
fix(fixer) {
return fixer.replaceText(attribute, `padding={{ default: 'noPadding' }}`);
}
});
}
} else {
// ToolbarToggleGroup
if (importName === 'ToolbarToggleGroup') {
const attribute = node.attributes.find(attr => attr.name && attr.name.name === 'breakpoint');
if (attribute) {
const breakpointVal = attribute.value.value;
const newAttr = `show={{ ${breakpointVal}: 'show' }}`
context.report({
node,
message: `breakpoint prop on ToolbarToggleGroup removed in favor of show={{ ${breakpointVal}: 'show' }}`,
fix(fixer) {
return fixer.replaceText(attribute, newAttr);
}
});
}
}
// breakpointMods prop
const attribute = node.attributes.find(attr => attr.name && attr.name.name === 'breakpointMods');
if (
attribute &&
attribute.value.type === 'JSXExpressionContainer' &&
attribute.value.expression &&
attribute.value.expression.type === 'ArrayExpression'
) {
const breakpointModsArr = attribute.value.expression.elements;
const newModifier = {};
breakpointModsArr.forEach(breakpointModObj => {
if (breakpointModObj.type === 'ObjectExpression') {
const modifierNode = breakpointModObj.properties.find(prop => prop.key.name === 'modifier');
const modVal = modifierNode.value;
const modifierValue = camelCase(
modVal.type === 'Literal' && modVal.value ||
modVal.type === 'MemberExpression' && modVal.property.type === 'Literal' && modVal.property.value ||
modVal.type === 'MemberExpression' && modVal.property.type === 'Identifier' && modVal.property.name
);
if (modifierValue) {
const modifierName = Object.keys(breakpointPropNames).find(propName => breakpointPropNames[propName].includes(modifierValue));
const breakpointNode = breakpointModObj.properties.find(prop => prop.key.name === 'breakpoint');
if (!newModifier[modifierName]) {
newModifier[modifierName] = {};
}
if (breakpointNode) {
const breakpointName = breakpointNode.value.value;
newModifier[modifierName][breakpointName] = modifierValue;
} else {
newModifier[modifierName].default = modifierValue;
}
}
}
});
const newProps = Object.entries(newModifier).reduce((acc, [key, val]) => {
return acc += `${key}={${JSON.stringify(val)}} `;
}, ``);
context.report({
node,
message: errorMessages[nodeName],
fix(fixer) {
return fixer.replaceText(attribute, newProps);
}
});
}
}
}
}
};
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ const removedImports = [
'ChipButton', // See chipgroup-remove-chipbutton rule for more info
'ChipGroupToolbarItem', // See chipgroup-remove-chipgrouptoolbaritem rule for more info,
'cellHeightAuto', // See table-removed-transforms rule for more info
'TitleSize' // See title-size rule for more info
'TitleSize', // See title-size rule for more info
'FlexModifiers', // See refactor-breakpointmods rule for more info
'FlexBreakpoints', // See refactor-breakpointmods rule for more info
'FlexBreakpointMod', // See refactor-breakpointmods rule for more info
'FlexItemBreakpointMod' // See refactor-breakpointmods rule for more info
];

// https://github.com/patternfly/pf-codemods/issues/39
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
const ruleTester = require('./ruletester');
const rule = require('../../lib/rules/refactor-breakpointmods');

ruleTester.run("refactor-breakpointmods", rule, {
valid: [
{
code: `import { Flex, FlexItem } from '@patternfly/react-core';
<Flex alignSelf={{default: "alignSelfStretch"}}>
<FlexItem alignSelf={{default: "alignSelfStretch"}}></FlexItem>
</Flex>`
},
{
code: `import { PageSection } from '@patternfly/react-core';
<PageSection padding={{ default: 'noPadding' }}></PageSection>`
},
{
code: `import { ToolbarItem } from '@patternfly/react-core';
<ToolbarItem visibility={{
default: 'visible',
lg: 'hidden'
}}></ToolbarItem>`
},
{
code: `import { ToolbarToggleGroup } from '@patternfly/react-core';
<ToolbarToggleGroup spacer={{default: "spacerNone"}} show={{xl: 'show'}}></ToolbarToggleGroup>`
}
],
invalid: [
{
code: `import { Flex, FlexItem, FlexModifiers } from '@patternfly/react-core';
<Flex breakpointMods={[
{modifier: FlexModifiers.column},
{modifier: FlexModifiers["align-self-stretch"]},
{modifier: 'spacer-none'},
{modifier: 'row', breakpoint: 'sm'}
]}>
<FlexItem breakpointMods={[
{breakpoint: 'xl', modifier: FlexModifiers.column},
{modifier: FlexModifiers["align-self-stretch"], breakpoint: 'md'},
{modifier: FlexModifiers.row}
]}></FlexItem>
</Flex>`,
output: `import { Flex, FlexItem, FlexModifiers } from '@patternfly/react-core';
<Flex direction={{"default":"column","sm":"row"}} alignSelf={{"default":"alignSelfStretch"}} spacer={{"default":"spacerNone"}} >
<FlexItem direction={{"xl":"column","default":"row"}} alignSelf={{"md":"alignSelfStretch"}} ></FlexItem>
</Flex>`,
errors: [
{
message: `Removed breakpointMods prop from Flex in favor of spacer, spaceItems, grow, shrink, flex, direction, alignItems, alignContent, alignSelf, align, justifyContent, display, fullWidth and flexWrap`,
type: "JSXOpeningElement",
},
{
message: `Removed breakpointMods prop from FlexItem in favor of spacer, grow, shrink, flex, alignSelf, align, and fullWidth`,
type: "JSXOpeningElement",
},
]
},
{
code: `import { PageSection as MyPageSection } from '@patternfly/react-core';
<MyPageSection hasNoPadding></MyPageSection>`,
output: `import { PageSection as MyPageSection } from '@patternfly/react-core';
<MyPageSection padding={{ default: 'noPadding' }}></MyPageSection>`,
errors: [{
message: `hasNoPadding prop on PageSection component removed in favor of padding={{ default: 'noPadding' }}`,
type: "JSXOpeningElement",
}]
},
{
code: `import { ToolbarItem } from '@patternfly/react-core';
<ToolbarItem breakpointMods={[
{modifier: 'spacer-none'},
{breakpoint: 'lg', modifier: 'spacer-lg'},
{modifier:"align-right", breakpoint: 'sm'}
]}></ToolbarItem>`,
output: `import { ToolbarItem } from '@patternfly/react-core';
<ToolbarItem spacer={{"default":"spacerNone","lg":"spacerLg"}} align={{"sm":"alignRight"}} ></ToolbarItem>`,
errors: [{
message: `Removed breakpointMods prop from ToolbarItem in favor of visiblity, alignment, and spacer`,
type: "JSXOpeningElement",
}]
},
{
code: `import { ToolbarToggleGroup } from '@patternfly/react-core';
<ToolbarToggleGroup breakpoint="xl" breakpointMods={[
{ modifier: 'visible' },
{ modifier: 'hidden', breakpoint: 'lg' }
]}></ToolbarToggleGroup>`,
output: `import { ToolbarToggleGroup } from '@patternfly/react-core';
<ToolbarToggleGroup show={{ xl: 'show' }} visibility={{"default":"visible","lg":"hidden"}} ></ToolbarToggleGroup>`,
errors: [
{
message: `breakpoint prop on ToolbarToggleGroup removed in favor of show={{ xl: 'show' }}`,
type: "JSXOpeningElement",
},
{
message: `Removed breakpointMods prop from ToolbarToggleGroup in favor of visiblity, alignment, spacer, and spaceItems`,
type: "JSXOpeningElement",
}
]
},
]
});

0 comments on commit e2a02e3

Please sign in to comment.