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:
Valera Melnikov 2024-12-19 10:22:39 +03:00 committed by GitHub
parent 8ec314f233
commit ad810c0811
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 239 additions and 16 deletions

View File

@ -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) => (

View File

@ -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) => (

View File

@ -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",

View File

@ -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: {

View File

@ -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;

View File

@ -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;

View File

@ -21,7 +21,7 @@ import {
*/
const meta: Meta<typeof InlineButtons> = {
component: InlineButtons,
title: "WDS/Widgets/InlineButtons",
title: "WDS/Widgets/Inline Buttons",
};
export default meta;

View File

@ -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;

View File

@ -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: {

View File

@ -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,
},

View File

@ -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: {

View File

@ -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: {

View File

@ -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: {

View File

@ -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: {

View File

@ -21,7 +21,7 @@ import {
*/
const meta: Meta<typeof ToolbarButtons> = {
component: ToolbarButtons,
title: "WDS/Widgets/ToolbarButtons",
title: "WDS/Widgets/Toolbar Buttons",
};
export default meta;

View File

@ -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",

View File

@ -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" };`,
},
],
});

View File

@ -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);
}
}
});
},
};
},
};

View File

@ -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",
},
},
},