diff --git a/app/client/packages/design-system/ads/src/NumberInput/NumberInput.stories.tsx b/app/client/packages/design-system/ads/src/NumberInput/NumberInput.stories.tsx index 543d7531c0..350fb658ed 100644 --- a/app/client/packages/design-system/ads/src/NumberInput/NumberInput.stories.tsx +++ b/app/client/packages/design-system/ads/src/NumberInput/NumberInput.stories.tsx @@ -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) => ( diff --git a/app/client/packages/design-system/ads/src/SearchInput/SearchInput.stories.tsx b/app/client/packages/design-system/ads/src/SearchInput/SearchInput.stories.tsx index b6399f4ceb..306e0e64a0 100644 --- a/app/client/packages/design-system/ads/src/SearchInput/SearchInput.stories.tsx +++ b/app/client/packages/design-system/ads/src/SearchInput/SearchInput.stories.tsx @@ -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) => ( diff --git a/app/client/packages/design-system/ads/src/Templates/IDEHeader/IDEHeader.stories.tsx b/app/client/packages/design-system/ads/src/Templates/IDEHeader/IDEHeader.stories.tsx index 6c709080dd..c24b5e8eaf 100644 --- a/app/client/packages/design-system/ads/src/Templates/IDEHeader/IDEHeader.stories.tsx +++ b/app/client/packages/design-system/ads/src/Templates/IDEHeader/IDEHeader.stories.tsx @@ -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", diff --git a/app/client/packages/design-system/widgets/src/components/ComboBox/stories/ComboBox.stories.tsx b/app/client/packages/design-system/widgets/src/components/ComboBox/stories/ComboBox.stories.tsx index 5f01ae804c..36a64aa1c2 100644 --- a/app/client/packages/design-system/widgets/src/components/ComboBox/stories/ComboBox.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/ComboBox/stories/ComboBox.stories.tsx @@ -6,7 +6,7 @@ import { ComboBox, ListBoxItem, Flex, Button } from "@appsmith/wds"; import { items } from "./items"; const meta: Meta = { - title: "WDS/Widgets/ComboBox", + title: "WDS/Widgets/Combo Box", component: ComboBox, tags: ["autodocs"], args: { diff --git a/app/client/packages/design-system/widgets/src/components/Datepicker/stories/Datepicker.stories.tsx b/app/client/packages/design-system/widgets/src/components/Datepicker/stories/Datepicker.stories.tsx index bfb0428959..37137ed022 100644 --- a/app/client/packages/design-system/widgets/src/components/Datepicker/stories/Datepicker.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/Datepicker/stories/Datepicker.stories.tsx @@ -10,7 +10,7 @@ import { DatePicker } from "../src"; */ const meta: Meta = { component: DatePicker, - title: "WDS/Widgets/DatePicker", + title: "WDS/Widgets/Date Picker", }; export default meta; diff --git a/app/client/packages/design-system/widgets/src/components/IconButton/stories/IconButton.stories.tsx b/app/client/packages/design-system/widgets/src/components/IconButton/stories/IconButton.stories.tsx index 96fdcf0343..a5241ff2ca 100644 --- a/app/client/packages/design-system/widgets/src/components/IconButton/stories/IconButton.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/IconButton/stories/IconButton.stories.tsx @@ -14,7 +14,7 @@ import { objectKeys } from "@appsmith/utils"; */ const meta: Meta = { component: IconButton, - title: "WDS/Widgets/IconButton", + title: "WDS/Widgets/Icon Button", }; export default meta; diff --git a/app/client/packages/design-system/widgets/src/components/InlineButtons/stories/InlineButtons.stories.tsx b/app/client/packages/design-system/widgets/src/components/InlineButtons/stories/InlineButtons.stories.tsx index 44a274739d..c30b43e51a 100644 --- a/app/client/packages/design-system/widgets/src/components/InlineButtons/stories/InlineButtons.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/InlineButtons/stories/InlineButtons.stories.tsx @@ -21,7 +21,7 @@ import { */ const meta: Meta = { component: InlineButtons, - title: "WDS/Widgets/InlineButtons", + title: "WDS/Widgets/Inline Buttons", }; export default meta; diff --git a/app/client/packages/design-system/widgets/src/components/RadioGroup/chromatic/RadioGroup.chromatic.stories.tsx b/app/client/packages/design-system/widgets/src/components/RadioGroup/chromatic/RadioGroup.chromatic.stories.tsx index 2225cc3f1e..86e63c2f8c 100644 --- a/app/client/packages/design-system/widgets/src/components/RadioGroup/chromatic/RadioGroup.chromatic.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/RadioGroup/chromatic/RadioGroup.chromatic.stories.tsx @@ -6,7 +6,7 @@ import { StoryGrid, DataAttrWrapper } from "@design-system/storybook"; const meta: Meta = { component: RadioGroup, - title: "Design System/Widgets/RadioGroup", + title: "Design System/Widgets/Radio Group", }; export default meta; diff --git a/app/client/packages/design-system/widgets/src/components/RadioGroup/stories/RadioGroup.stories.tsx b/app/client/packages/design-system/widgets/src/components/RadioGroup/stories/RadioGroup.stories.tsx index e16fdfbc18..9ed7e7417f 100644 --- a/app/client/packages/design-system/widgets/src/components/RadioGroup/stories/RadioGroup.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/RadioGroup/stories/RadioGroup.stories.tsx @@ -8,7 +8,7 @@ const items = [ ]; const meta: Meta = { - title: "WDS/Widgets/RadioGroup", + title: "WDS/Widgets/Radio Group", component: RadioGroup, tags: ["autodocs"], args: { diff --git a/app/client/packages/design-system/widgets/src/components/TagGroup/stories/TagGroup.stories.tsx b/app/client/packages/design-system/widgets/src/components/TagGroup/stories/TagGroup.stories.tsx index 2325e2b81e..0fa6d3d778 100644 --- a/app/client/packages/design-system/widgets/src/components/TagGroup/stories/TagGroup.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/TagGroup/stories/TagGroup.stories.tsx @@ -8,7 +8,7 @@ import type { TagGroupProps } from "../src/TagGroup"; */ const meta: Meta = { component: TagGroup, - title: "WDS/Widgets/TagGroup", + title: "WDS/Widgets/Tag Group", subcomponents: { Tag, }, diff --git a/app/client/packages/design-system/widgets/src/components/TextArea/stories/TextArea.stories.tsx b/app/client/packages/design-system/widgets/src/components/TextArea/stories/TextArea.stories.tsx index b2b66dc45a..7cbd9126b4 100644 --- a/app/client/packages/design-system/widgets/src/components/TextArea/stories/TextArea.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/TextArea/stories/TextArea.stories.tsx @@ -4,7 +4,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Flex, TextArea, Button } from "@appsmith/wds"; const meta: Meta = { - title: "WDS/Widgets/TextArea", + title: "WDS/Widgets/Text Area", component: TextArea, tags: ["autodocs"], args: { diff --git a/app/client/packages/design-system/widgets/src/components/TextField/stories/TextField.stories.tsx b/app/client/packages/design-system/widgets/src/components/TextField/stories/TextField.stories.tsx index 0d99286e51..f623264448 100644 --- a/app/client/packages/design-system/widgets/src/components/TextField/stories/TextField.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/TextField/stories/TextField.stories.tsx @@ -4,7 +4,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Flex, Icon, TextField, Button } from "@appsmith/wds"; const meta: Meta = { - title: "WDS/Widgets/TextField", + title: "WDS/Widgets/Text Field", component: TextField, tags: ["autodocs"], args: { diff --git a/app/client/packages/design-system/widgets/src/components/TimeField/stories/TimeField.stories.tsx b/app/client/packages/design-system/widgets/src/components/TimeField/stories/TimeField.stories.tsx index a498cd5826..3c1b13b70b 100644 --- a/app/client/packages/design-system/widgets/src/components/TimeField/stories/TimeField.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/TimeField/stories/TimeField.stories.tsx @@ -4,7 +4,7 @@ import { TimeField } from "../src"; import { Time } from "@internationalized/date"; const meta: Meta = { - title: "WDS/Widgets/TimeField", + title: "WDS/Widgets/Time Field", component: TimeField, parameters: { docs: { diff --git a/app/client/packages/design-system/widgets/src/components/ToggleGroup/stories/ToggleGroup.stories.tsx b/app/client/packages/design-system/widgets/src/components/ToggleGroup/stories/ToggleGroup.stories.tsx index 5db40b2102..995ed35bd5 100644 --- a/app/client/packages/design-system/widgets/src/components/ToggleGroup/stories/ToggleGroup.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/ToggleGroup/stories/ToggleGroup.stories.tsx @@ -8,7 +8,7 @@ const items = [ ]; const meta: Meta = { - title: "WDS/Widgets/ToggleGroup", + title: "WDS/Widgets/Toggle Group", component: ToggleGroup, tags: ["autodocs"], args: { diff --git a/app/client/packages/design-system/widgets/src/components/ToolbarButtons/stories/ToolbarButtons.stories.tsx b/app/client/packages/design-system/widgets/src/components/ToolbarButtons/stories/ToolbarButtons.stories.tsx index 917338e6b3..2fdeaa192b 100644 --- a/app/client/packages/design-system/widgets/src/components/ToolbarButtons/stories/ToolbarButtons.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/ToolbarButtons/stories/ToolbarButtons.stories.tsx @@ -21,7 +21,7 @@ import { */ const meta: Meta = { component: ToolbarButtons, - title: "WDS/Widgets/ToolbarButtons", + title: "WDS/Widgets/Toolbar Buttons", }; export default meta; diff --git a/app/client/packages/design-system/widgets/src/testing/ColorGrid.stories.tsx b/app/client/packages/design-system/widgets/src/testing/ColorGrid.stories.tsx index d67a774feb..977f95427f 100644 --- a/app/client/packages/design-system/widgets/src/testing/ColorGrid.stories.tsx +++ b/app/client/packages/design-system/widgets/src/testing/ColorGrid.stories.tsx @@ -4,7 +4,7 @@ import { ColorGrid } from "./ColorGrid"; const meta: Meta = { component: ColorGrid, - title: "WDS/Testing/ColorGrid", + title: "WDS/Testing/Color Grid", args: { source: "oklch", size: "small", diff --git a/app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.test.ts b/app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.test.ts new file mode 100644 index 0000000000..b55b7948a2 --- /dev/null +++ b/app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.test.ts @@ -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" };`, + }, + ], +}); diff --git a/app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.ts b/app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.ts new file mode 100644 index 0000000000..aed34b0eb2 --- /dev/null +++ b/app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.ts @@ -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); + } + } + }); + }, + }; + }, + }; diff --git a/app/client/packages/eslint-plugin/src/index.ts b/app/client/packages/eslint-plugin/src/index.ts index eda6deb6d3..49fb2417f4 100644 --- a/app/client/packages/eslint-plugin/src/index.ts +++ b/app/client/packages/eslint-plugin/src/index.ts @@ -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", }, }, },