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";
|
||||
|
||||
export default {
|
||||
title: "ADS/Components/Input/NumberInput",
|
||||
title: "ADS/Components/Input/Number Input",
|
||||
component: NumberInput,
|
||||
decorators: [
|
||||
(Story: () => React.ReactNode) => (
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { SearchInputProps } from "./SearchInput.types";
|
|||
import type { StoryObj } from "@storybook/react";
|
||||
|
||||
export default {
|
||||
title: "ADS/Components/Input/SearchInput",
|
||||
title: "ADS/Components/Input/Search Input",
|
||||
component: SearchInput,
|
||||
decorators: [
|
||||
(Story: () => React.ReactNode) => (
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { Text } from "../../Text";
|
|||
import { ListHeaderContainer } from "../EntityExplorer/styles";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "ADS/Templates/IDEHeader",
|
||||
title: "ADS/Templates/IDE Header",
|
||||
component: IDEHeader,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { ComboBox, ListBoxItem, Flex, Button } from "@appsmith/wds";
|
|||
import { items } from "./items";
|
||||
|
||||
const meta: Meta<typeof ComboBox> = {
|
||||
title: "WDS/Widgets/ComboBox",
|
||||
title: "WDS/Widgets/Combo Box",
|
||||
component: ComboBox,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { DatePicker } from "../src";
|
|||
*/
|
||||
const meta: Meta<typeof DatePicker> = {
|
||||
component: DatePicker,
|
||||
title: "WDS/Widgets/DatePicker",
|
||||
title: "WDS/Widgets/Date Picker",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { objectKeys } from "@appsmith/utils";
|
|||
*/
|
||||
const meta: Meta<typeof IconButton> = {
|
||||
component: IconButton,
|
||||
title: "WDS/Widgets/IconButton",
|
||||
title: "WDS/Widgets/Icon Button",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
*/
|
||||
const meta: Meta<typeof InlineButtons> = {
|
||||
component: InlineButtons,
|
||||
title: "WDS/Widgets/InlineButtons",
|
||||
title: "WDS/Widgets/Inline Buttons",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { StoryGrid, DataAttrWrapper } from "@design-system/storybook";
|
|||
|
||||
const meta: Meta<typeof RadioGroup> = {
|
||||
component: RadioGroup,
|
||||
title: "Design System/Widgets/RadioGroup",
|
||||
title: "Design System/Widgets/Radio Group",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const items = [
|
|||
];
|
||||
|
||||
const meta: Meta<typeof RadioGroup> = {
|
||||
title: "WDS/Widgets/RadioGroup",
|
||||
title: "WDS/Widgets/Radio Group",
|
||||
component: RadioGroup,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import type { TagGroupProps } from "../src/TagGroup";
|
|||
*/
|
||||
const meta: Meta<typeof TagGroup> = {
|
||||
component: TagGroup,
|
||||
title: "WDS/Widgets/TagGroup",
|
||||
title: "WDS/Widgets/Tag Group",
|
||||
subcomponents: {
|
||||
Tag,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
|||
import { Flex, TextArea, Button } from "@appsmith/wds";
|
||||
|
||||
const meta: Meta<typeof TextArea> = {
|
||||
title: "WDS/Widgets/TextArea",
|
||||
title: "WDS/Widgets/Text Area",
|
||||
component: TextArea,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
|||
import { Flex, Icon, TextField, Button } from "@appsmith/wds";
|
||||
|
||||
const meta: Meta<typeof TextField> = {
|
||||
title: "WDS/Widgets/TextField",
|
||||
title: "WDS/Widgets/Text Field",
|
||||
component: TextField,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { TimeField } from "../src";
|
|||
import { Time } from "@internationalized/date";
|
||||
|
||||
const meta: Meta<typeof TimeField> = {
|
||||
title: "WDS/Widgets/TimeField",
|
||||
title: "WDS/Widgets/Time Field",
|
||||
component: TimeField,
|
||||
parameters: {
|
||||
docs: {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const items = [
|
|||
];
|
||||
|
||||
const meta: Meta<typeof ToggleGroup> = {
|
||||
title: "WDS/Widgets/ToggleGroup",
|
||||
title: "WDS/Widgets/Toggle Group",
|
||||
component: ToggleGroup,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
*/
|
||||
const meta: Meta<typeof ToolbarButtons> = {
|
||||
component: ToolbarButtons,
|
||||
title: "WDS/Widgets/ToolbarButtons",
|
||||
title: "WDS/Widgets/Toolbar Buttons",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { ColorGrid } from "./ColorGrid";
|
|||
|
||||
const meta: Meta<typeof ColorGrid> = {
|
||||
component: ColorGrid,
|
||||
title: "WDS/Testing/ColorGrid",
|
||||
title: "WDS/Testing/Color Grid",
|
||||
args: {
|
||||
source: "oklch",
|
||||
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 { namedUseEffectRule } from "./named-use-effect/rule";
|
||||
import { consistentStorybookTitle } from "./consistent-storybook-title/rule";
|
||||
|
||||
const plugin = {
|
||||
rules: {
|
||||
"object-keys": objectKeysRule,
|
||||
"named-use-effect": namedUseEffectRule,
|
||||
"consistent-storybook-title": consistentStorybookTitle,
|
||||
},
|
||||
configs: {
|
||||
recommended: {
|
||||
rules: {
|
||||
"@appsmith/object-keys": "warn",
|
||||
"@appsmith/named-use-effect": "warn",
|
||||
"@appsmith/consistent-storybook-title": "error",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user