chore: add custom consistent-storybook-title rule (#38241)
## Automation /ok-to-test tags="@tag.Sanity" ### 🔍 Cypress test results <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/12396717822> > Commit: c556bda0ecbae89388821185ab86d340c553bc1e > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=12396717822&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.Sanity` > Spec: > <hr>Wed, 18 Dec 2024 16:46:31 UTC <!-- end of auto-generated comment: Cypress test results --> ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [x] No <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Updated titles for various components in Storybook for improved readability (e.g., "NumberInput" to "Number Input"). - Introduced a new ESLint rule to enforce Title Case formatting for Storybook titles. - **Bug Fixes** - Enhanced error handling and validation for Storybook titles through the new ESLint rule. - **Documentation** - Added test cases for the new ESLint rule to validate title formats in Storybook configurations. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
8ec314f233
commit
ad810c0811
|
|
@ -4,7 +4,7 @@ import type { NumberInputProps } from "./NumberInput.types";
|
||||||
import type { StoryObj } from "@storybook/react";
|
import type { StoryObj } from "@storybook/react";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "ADS/Components/Input/NumberInput",
|
title: "ADS/Components/Input/Number Input",
|
||||||
component: NumberInput,
|
component: NumberInput,
|
||||||
decorators: [
|
decorators: [
|
||||||
(Story: () => React.ReactNode) => (
|
(Story: () => React.ReactNode) => (
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import type { SearchInputProps } from "./SearchInput.types";
|
||||||
import type { StoryObj } from "@storybook/react";
|
import type { StoryObj } from "@storybook/react";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "ADS/Components/Input/SearchInput",
|
title: "ADS/Components/Input/Search Input",
|
||||||
component: SearchInput,
|
component: SearchInput,
|
||||||
decorators: [
|
decorators: [
|
||||||
(Story: () => React.ReactNode) => (
|
(Story: () => React.ReactNode) => (
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { Text } from "../../Text";
|
||||||
import { ListHeaderContainer } from "../EntityExplorer/styles";
|
import { ListHeaderContainer } from "../EntityExplorer/styles";
|
||||||
|
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
title: "ADS/Templates/IDEHeader",
|
title: "ADS/Templates/IDE Header",
|
||||||
component: IDEHeader,
|
component: IDEHeader,
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: "fullscreen",
|
layout: "fullscreen",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { ComboBox, ListBoxItem, Flex, Button } from "@appsmith/wds";
|
||||||
import { items } from "./items";
|
import { items } from "./items";
|
||||||
|
|
||||||
const meta: Meta<typeof ComboBox> = {
|
const meta: Meta<typeof ComboBox> = {
|
||||||
title: "WDS/Widgets/ComboBox",
|
title: "WDS/Widgets/Combo Box",
|
||||||
component: ComboBox,
|
component: ComboBox,
|
||||||
tags: ["autodocs"],
|
tags: ["autodocs"],
|
||||||
args: {
|
args: {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { DatePicker } from "../src";
|
||||||
*/
|
*/
|
||||||
const meta: Meta<typeof DatePicker> = {
|
const meta: Meta<typeof DatePicker> = {
|
||||||
component: DatePicker,
|
component: DatePicker,
|
||||||
title: "WDS/Widgets/DatePicker",
|
title: "WDS/Widgets/Date Picker",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { objectKeys } from "@appsmith/utils";
|
||||||
*/
|
*/
|
||||||
const meta: Meta<typeof IconButton> = {
|
const meta: Meta<typeof IconButton> = {
|
||||||
component: IconButton,
|
component: IconButton,
|
||||||
title: "WDS/Widgets/IconButton",
|
title: "WDS/Widgets/Icon Button",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import {
|
||||||
*/
|
*/
|
||||||
const meta: Meta<typeof InlineButtons> = {
|
const meta: Meta<typeof InlineButtons> = {
|
||||||
component: InlineButtons,
|
component: InlineButtons,
|
||||||
title: "WDS/Widgets/InlineButtons",
|
title: "WDS/Widgets/Inline Buttons",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { StoryGrid, DataAttrWrapper } from "@design-system/storybook";
|
||||||
|
|
||||||
const meta: Meta<typeof RadioGroup> = {
|
const meta: Meta<typeof RadioGroup> = {
|
||||||
component: RadioGroup,
|
component: RadioGroup,
|
||||||
title: "Design System/Widgets/RadioGroup",
|
title: "Design System/Widgets/Radio Group",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const items = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const meta: Meta<typeof RadioGroup> = {
|
const meta: Meta<typeof RadioGroup> = {
|
||||||
title: "WDS/Widgets/RadioGroup",
|
title: "WDS/Widgets/Radio Group",
|
||||||
component: RadioGroup,
|
component: RadioGroup,
|
||||||
tags: ["autodocs"],
|
tags: ["autodocs"],
|
||||||
args: {
|
args: {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import type { TagGroupProps } from "../src/TagGroup";
|
||||||
*/
|
*/
|
||||||
const meta: Meta<typeof TagGroup> = {
|
const meta: Meta<typeof TagGroup> = {
|
||||||
component: TagGroup,
|
component: TagGroup,
|
||||||
title: "WDS/Widgets/TagGroup",
|
title: "WDS/Widgets/Tag Group",
|
||||||
subcomponents: {
|
subcomponents: {
|
||||||
Tag,
|
Tag,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||||
import { Flex, TextArea, Button } from "@appsmith/wds";
|
import { Flex, TextArea, Button } from "@appsmith/wds";
|
||||||
|
|
||||||
const meta: Meta<typeof TextArea> = {
|
const meta: Meta<typeof TextArea> = {
|
||||||
title: "WDS/Widgets/TextArea",
|
title: "WDS/Widgets/Text Area",
|
||||||
component: TextArea,
|
component: TextArea,
|
||||||
tags: ["autodocs"],
|
tags: ["autodocs"],
|
||||||
args: {
|
args: {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||||
import { Flex, Icon, TextField, Button } from "@appsmith/wds";
|
import { Flex, Icon, TextField, Button } from "@appsmith/wds";
|
||||||
|
|
||||||
const meta: Meta<typeof TextField> = {
|
const meta: Meta<typeof TextField> = {
|
||||||
title: "WDS/Widgets/TextField",
|
title: "WDS/Widgets/Text Field",
|
||||||
component: TextField,
|
component: TextField,
|
||||||
tags: ["autodocs"],
|
tags: ["autodocs"],
|
||||||
args: {
|
args: {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { TimeField } from "../src";
|
||||||
import { Time } from "@internationalized/date";
|
import { Time } from "@internationalized/date";
|
||||||
|
|
||||||
const meta: Meta<typeof TimeField> = {
|
const meta: Meta<typeof TimeField> = {
|
||||||
title: "WDS/Widgets/TimeField",
|
title: "WDS/Widgets/Time Field",
|
||||||
component: TimeField,
|
component: TimeField,
|
||||||
parameters: {
|
parameters: {
|
||||||
docs: {
|
docs: {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const items = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const meta: Meta<typeof ToggleGroup> = {
|
const meta: Meta<typeof ToggleGroup> = {
|
||||||
title: "WDS/Widgets/ToggleGroup",
|
title: "WDS/Widgets/Toggle Group",
|
||||||
component: ToggleGroup,
|
component: ToggleGroup,
|
||||||
tags: ["autodocs"],
|
tags: ["autodocs"],
|
||||||
args: {
|
args: {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import {
|
||||||
*/
|
*/
|
||||||
const meta: Meta<typeof ToolbarButtons> = {
|
const meta: Meta<typeof ToolbarButtons> = {
|
||||||
component: ToolbarButtons,
|
component: ToolbarButtons,
|
||||||
title: "WDS/Widgets/ToolbarButtons",
|
title: "WDS/Widgets/Toolbar Buttons",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { ColorGrid } from "./ColorGrid";
|
||||||
|
|
||||||
const meta: Meta<typeof ColorGrid> = {
|
const meta: Meta<typeof ColorGrid> = {
|
||||||
component: ColorGrid,
|
component: ColorGrid,
|
||||||
title: "WDS/Testing/ColorGrid",
|
title: "WDS/Testing/Color Grid",
|
||||||
args: {
|
args: {
|
||||||
source: "oklch",
|
source: "oklch",
|
||||||
size: "small",
|
size: "small",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
import { TSESLint } from "@typescript-eslint/utils";
|
||||||
|
import { consistentStorybookTitle } from "./rule";
|
||||||
|
|
||||||
|
const ruleTester = new TSESLint.RuleTester({
|
||||||
|
parser: require.resolve("@typescript-eslint/parser"),
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
ruleTester.run("storybook-title-case", consistentStorybookTitle, {
|
||||||
|
valid: [
|
||||||
|
{
|
||||||
|
code: `export default { title: "ADS/Templates/IDE Header" };`,
|
||||||
|
filename: "example.stories.tsx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `export default { title: "ADS/Components/Input/IDE Search Input" };`,
|
||||||
|
filename: "example.stories.tsx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `export default { title: "ADS/Components/Input/AAA Number Input" };`,
|
||||||
|
filename: "example.stories.tsx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `const meta = { title: "WDS/Widgets/Button" };`,
|
||||||
|
filename: "example.stories.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: `export default { title: "ADS/Templates/IDEHeader" };`,
|
||||||
|
filename: "example.stories.tsx",
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: "invalidTitle",
|
||||||
|
data: {
|
||||||
|
title: "ADS/Templates/IDEHeader",
|
||||||
|
suggestedTitle: "ADS/Templates/IDE Header",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
output: `export default { title: "ADS/Templates/IDE Header" };`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `export default { title: "ADS/Components/Input/IDESearch Input" };`,
|
||||||
|
filename: "example.stories.tsx",
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: "invalidTitle",
|
||||||
|
data: {
|
||||||
|
title: "ADS/Components/Input/IDESearch Input",
|
||||||
|
suggestedTitle: "ADS/Components/Input/IDE Search Input",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
output: `export default { title: "ADS/Components/Input/IDE Search Input" };`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `export default { title: "ADS/Components/Input/IDESearchInput" };`,
|
||||||
|
filename: "example.stories.tsx",
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: "invalidTitle",
|
||||||
|
data: {
|
||||||
|
title: "ADS/Components/Input/IDESearchInput",
|
||||||
|
suggestedTitle: "ADS/Components/Input/IDE Search Input",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
output: `export default { title: "ADS/Components/Input/IDE Search Input" };`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `export default { title: "ADS/Components/Input/AAANumber Input" };`,
|
||||||
|
filename: "example.stories.tsx",
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: "invalidTitle",
|
||||||
|
data: {
|
||||||
|
title: "ADS/Components/Input/AAANumber Input",
|
||||||
|
suggestedTitle: "ADS/Components/Input/AAA Number Input",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
output: `export default { title: "ADS/Components/Input/AAA Number Input" };`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `export default { title: "WDS/Widgets/button" };`,
|
||||||
|
filename: "example.stories.tsx",
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: "invalidTitle",
|
||||||
|
data: {
|
||||||
|
title: "WDS/Widgets/button",
|
||||||
|
suggestedTitle: "WDS/Widgets/Button",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
output: `export default { title: "WDS/Widgets/Button" };`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
import type { TSESLint, TSESTree } from "@typescript-eslint/utils";
|
||||||
|
|
||||||
|
const titleCase = (str: string): string => {
|
||||||
|
return str
|
||||||
|
.split("/")
|
||||||
|
.map(
|
||||||
|
(segment) =>
|
||||||
|
segment
|
||||||
|
.replace(/([A-Z]+)([A-Z][a-z0-9])/g, "$1 $2") // Acronyms followed by words
|
||||||
|
.replace(/([a-z0-9])([A-Z])/g, "$1 $2") // Lowercase followed by uppercase
|
||||||
|
.replace(/\b\w/g, (char) => char.toUpperCase()), // Capitalize first letter
|
||||||
|
)
|
||||||
|
.join("/");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const consistentStorybookTitle: TSESLint.RuleModule<"invalidTitle", []> =
|
||||||
|
{
|
||||||
|
defaultOptions: [],
|
||||||
|
meta: {
|
||||||
|
type: "problem",
|
||||||
|
docs: {
|
||||||
|
description:
|
||||||
|
"Ensure Storybook titles in `export default` or meta objects are in Title Case",
|
||||||
|
recommended: "error",
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
invalidTitle:
|
||||||
|
'The Storybook title "{{ title }}" is not in Title Case. Suggested: "{{ suggestedTitle }}".',
|
||||||
|
},
|
||||||
|
schema: [], // No options
|
||||||
|
fixable: "code", // Allows auto-fixing
|
||||||
|
},
|
||||||
|
|
||||||
|
create(context) {
|
||||||
|
const filename = context.getFilename();
|
||||||
|
const isStoryFile = filename.endsWith(".stories.tsx"); // Apply only to *.stories.tsx files
|
||||||
|
|
||||||
|
if (!isStoryFile) {
|
||||||
|
return {}; // No-op for non-story files
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateTitle = (title: string, node: TSESTree.Node) => {
|
||||||
|
const expectedTitle = titleCase(title);
|
||||||
|
|
||||||
|
if (title !== expectedTitle) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: "invalidTitle",
|
||||||
|
data: {
|
||||||
|
title,
|
||||||
|
suggestedTitle: expectedTitle,
|
||||||
|
},
|
||||||
|
fix: (fixer) => fixer.replaceText(node, `"${expectedTitle}"`), // Use double quotes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
ExportDefaultDeclaration(node: TSESTree.ExportDefaultDeclaration) {
|
||||||
|
if (
|
||||||
|
node.declaration.type === "ObjectExpression" &&
|
||||||
|
node.declaration.properties.some(
|
||||||
|
(prop) =>
|
||||||
|
prop.type === "Property" &&
|
||||||
|
prop.key.type === "Identifier" &&
|
||||||
|
prop.key.name === "title" &&
|
||||||
|
prop.value.type === "Literal" &&
|
||||||
|
typeof prop.value.value === "string",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const titleProperty = node.declaration.properties.find(
|
||||||
|
(prop) =>
|
||||||
|
prop.type === "Property" &&
|
||||||
|
prop.key.type === "Identifier" &&
|
||||||
|
prop.key.name === "title",
|
||||||
|
) as TSESTree.Property;
|
||||||
|
|
||||||
|
const titleValue = titleProperty.value as TSESTree.Literal;
|
||||||
|
|
||||||
|
if (typeof titleValue.value === "string") {
|
||||||
|
validateTitle(titleValue.value, titleValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
VariableDeclaration(node: TSESTree.VariableDeclaration) {
|
||||||
|
node.declarations.forEach((declaration) => {
|
||||||
|
if (
|
||||||
|
declaration.init &&
|
||||||
|
declaration.init.type === "ObjectExpression" &&
|
||||||
|
declaration.init.properties.some(
|
||||||
|
(prop) =>
|
||||||
|
prop.type === "Property" &&
|
||||||
|
prop.key.type === "Identifier" &&
|
||||||
|
prop.key.name === "title" &&
|
||||||
|
prop.value.type === "Literal" &&
|
||||||
|
typeof prop.value.value === "string",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const titleProperty = declaration.init.properties.find(
|
||||||
|
(prop) =>
|
||||||
|
prop.type === "Property" &&
|
||||||
|
prop.key.type === "Identifier" &&
|
||||||
|
prop.key.name === "title",
|
||||||
|
) as TSESTree.Property;
|
||||||
|
|
||||||
|
const titleValue = titleProperty.value as TSESTree.Literal;
|
||||||
|
|
||||||
|
if (typeof titleValue.value === "string") {
|
||||||
|
validateTitle(titleValue.value, titleValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
import { objectKeysRule } from "./object-keys/rule";
|
import { objectKeysRule } from "./object-keys/rule";
|
||||||
import { namedUseEffectRule } from "./named-use-effect/rule";
|
import { namedUseEffectRule } from "./named-use-effect/rule";
|
||||||
|
import { consistentStorybookTitle } from "./consistent-storybook-title/rule";
|
||||||
|
|
||||||
const plugin = {
|
const plugin = {
|
||||||
rules: {
|
rules: {
|
||||||
"object-keys": objectKeysRule,
|
"object-keys": objectKeysRule,
|
||||||
"named-use-effect": namedUseEffectRule,
|
"named-use-effect": namedUseEffectRule,
|
||||||
|
"consistent-storybook-title": consistentStorybookTitle,
|
||||||
},
|
},
|
||||||
configs: {
|
configs: {
|
||||||
recommended: {
|
recommended: {
|
||||||
rules: {
|
rules: {
|
||||||
"@appsmith/object-keys": "warn",
|
"@appsmith/object-keys": "warn",
|
||||||
"@appsmith/named-use-effect": "warn",
|
"@appsmith/named-use-effect": "warn",
|
||||||
|
"@appsmith/consistent-storybook-title": "error",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user