From cb05b1fbc9cc036ca22d37beecf46f084ee95802 Mon Sep 17 00:00:00 2001 From: Aditya Pimpalkar Date: Tue, 19 Sep 2023 23:31:21 +0100 Subject: [PATCH] feat: reorder columns from table options (#1636) * draggable prop addition * draggable component addition * state modification * drag select state addition * changed state name * main merged * lint fix --------- Co-authored-by: Charles Bochet --- .../BoardOptionsDropdownContent.tsx | 2 + front/src/modules/ui/icon/index.ts | 1 + .../ui/menu-item/components/MenuItem.tsx | 8 +- .../components/MenuItemLeftContent.tsx | 14 +- .../ui/table/components/EntityTable.tsx | 18 +- .../modules/ui/table/hooks/useTableColumns.ts | 14 ++ .../components/TableOptionsDropdownButton.tsx | 16 +- .../TableOptionsDropdownContent.tsx | 24 ++- .../states/isDraggingAndSelectingState.ts | 6 + .../table-header/components/TableHeader.tsx | 5 + .../ui/view-bar/components/ViewBarDetails.tsx | 8 +- .../ViewFieldsVisibilityDropdownSection.tsx | 105 ++++++++-- .../eslint-plugin-twenty-ts/dist/index.js | 11 ++ .../dist/src/rules/effect-components.js | 96 +++++++++ .../dist/src/rules/matching-state-variable.js | 113 +++++++++++ .../dist/src/rules/no-hardcoded-colors.js | 42 ++++ .../sort-css-properties-alphabetically.js | 182 ++++++++++++++++++ .../styled-components-prefixed-with-styled.js | 49 +++++ .../dist/src/tests/effect-components.spec.js | 87 +++++++++ .../dist/src/tests/file.js | 3 + .../src/tests/matching-state-variable.spec.js | 50 +++++ .../src/tests/no-hardcoded-colors.spec.js | 45 +++++ .../dist/src/tests/react.js | 3 + ...sort-css-properties-alphabetically.spec.js | 61 ++++++ ...ed-components-prefixed-with-styled.spec.js | 49 +++++ 25 files changed, 979 insertions(+), 33 deletions(-) create mode 100644 front/src/modules/ui/table/states/isDraggingAndSelectingState.ts create mode 100644 packages/eslint-plugin-twenty-ts/dist/index.js create mode 100644 packages/eslint-plugin-twenty-ts/dist/src/rules/effect-components.js create mode 100644 packages/eslint-plugin-twenty-ts/dist/src/rules/matching-state-variable.js create mode 100644 packages/eslint-plugin-twenty-ts/dist/src/rules/no-hardcoded-colors.js create mode 100644 packages/eslint-plugin-twenty-ts/dist/src/rules/sort-css-properties-alphabetically.js create mode 100644 packages/eslint-plugin-twenty-ts/dist/src/rules/styled-components-prefixed-with-styled.js create mode 100644 packages/eslint-plugin-twenty-ts/dist/src/tests/effect-components.spec.js create mode 100644 packages/eslint-plugin-twenty-ts/dist/src/tests/file.js create mode 100644 packages/eslint-plugin-twenty-ts/dist/src/tests/matching-state-variable.spec.js create mode 100644 packages/eslint-plugin-twenty-ts/dist/src/tests/no-hardcoded-colors.spec.js create mode 100644 packages/eslint-plugin-twenty-ts/dist/src/tests/react.js create mode 100644 packages/eslint-plugin-twenty-ts/dist/src/tests/sort-css-properties-alphabetically.spec.js create mode 100644 packages/eslint-plugin-twenty-ts/dist/src/tests/styled-components-prefixed-with-styled.spec.js diff --git a/front/src/modules/ui/board/components/BoardOptionsDropdownContent.tsx b/front/src/modules/ui/board/components/BoardOptionsDropdownContent.tsx index be739fa4c..5c8fc800d 100644 --- a/front/src/modules/ui/board/components/BoardOptionsDropdownContent.tsx +++ b/front/src/modules/ui/board/components/BoardOptionsDropdownContent.tsx @@ -234,6 +234,7 @@ export const BoardOptionsDropdownContent = ({ title="Visible" fields={visibleBoardCardFields} onVisibilityChange={handleFieldVisibilityChange} + isDraggable={true} /> )} {hasVisibleFields && hasHiddenFields && ( @@ -244,6 +245,7 @@ export const BoardOptionsDropdownContent = ({ title="Hidden" fields={hiddenBoardCardFields} onVisibilityChange={handleFieldVisibilityChange} + isDraggable={false} /> )} diff --git a/front/src/modules/ui/icon/index.ts b/front/src/modules/ui/icon/index.ts index ef34a5134..bcdb0e84c 100644 --- a/front/src/modules/ui/icon/index.ts +++ b/front/src/modules/ui/icon/index.ts @@ -42,6 +42,7 @@ export { IconFileImport, IconFileUpload, IconForbid, + IconGripVertical, IconHeart, IconHelpCircle, IconInbox, diff --git a/front/src/modules/ui/menu-item/components/MenuItem.tsx b/front/src/modules/ui/menu-item/components/MenuItem.tsx index a41c84b1c..d23c05950 100644 --- a/front/src/modules/ui/menu-item/components/MenuItem.tsx +++ b/front/src/modules/ui/menu-item/components/MenuItem.tsx @@ -14,6 +14,7 @@ export type MenuItemIconButton = { }; export type MenuItemProps = { + isDraggable?: boolean; LeftIcon?: IconComponent | null; accent?: MenuItemAccent; text: string; @@ -35,6 +36,7 @@ const StyledHoverableMenuItemBase = styled(StyledMenuItemBase)` `; export const MenuItem = ({ + isDraggable, LeftIcon, accent = 'default', text, @@ -52,7 +54,11 @@ export const MenuItem = ({ className={className} accent={accent} > - +
{showIconButtons && ( diff --git a/front/src/modules/ui/menu-item/internals/components/MenuItemLeftContent.tsx b/front/src/modules/ui/menu-item/internals/components/MenuItemLeftContent.tsx index 6b4543b5d..c87ace4cd 100644 --- a/front/src/modules/ui/menu-item/internals/components/MenuItemLeftContent.tsx +++ b/front/src/modules/ui/menu-item/internals/components/MenuItemLeftContent.tsx @@ -1,5 +1,6 @@ import { useTheme } from '@emotion/react'; +import { IconGripVertical } from '@/ui/icon'; import { IconComponent } from '@/ui/icon/types/IconComponent'; import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip'; @@ -9,15 +10,26 @@ import { } from './StyledMenuItemBase'; type OwnProps = { + isDraggable?: boolean; LeftIcon: IconComponent | null | undefined; text: string; }; -export const MenuItemLeftContent = ({ LeftIcon, text }: OwnProps) => { +export const MenuItemLeftContent = ({ + isDraggable, + LeftIcon, + text, +}: OwnProps) => { const theme = useTheme(); return ( + {isDraggable && ( + + )} {LeftIcon && ( )} diff --git a/front/src/modules/ui/table/components/EntityTable.tsx b/front/src/modules/ui/table/components/EntityTable.tsx index 737940f0c..53573fa05 100644 --- a/front/src/modules/ui/table/components/EntityTable.tsx +++ b/front/src/modules/ui/table/components/EntityTable.tsx @@ -1,5 +1,6 @@ import { useRef } from 'react'; import styled from '@emotion/styled'; +import { useRecoilState } from 'recoil'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; @@ -14,6 +15,7 @@ import { useLeaveTableFocus } from '../hooks/useLeaveTableFocus'; import { useMapKeyboardToSoftFocus } from '../hooks/useMapKeyboardToSoftFocus'; import { useResetTableRowSelection } from '../hooks/useResetTableRowSelection'; import { useSetRowSelectedState } from '../hooks/useSetRowSelectedState'; +import { isDraggingAndSelectingState } from '../states/isDraggingAndSelectingState'; import { TableHeader } from '../table-header/components/TableHeader'; import { TableHotkeyScope } from '../types/TableHotkeyScope'; @@ -88,6 +90,9 @@ type OwnProps = { export const EntityTable = ({ updateEntityMutation }: OwnProps) => { const tableBodyRef = useRef(null); + const [isDraggingAndSelecting, setIsDraggingAndSelecting] = useRecoilState( + isDraggingAndSelectingState, + ); const setRowSelectedState = useSetRowSelectedState(); const resetTableRowSelection = useResetTableRowSelection(); @@ -100,6 +105,7 @@ export const EntityTable = ({ updateEntityMutation }: OwnProps) => { refs: [tableBodyRef], callback: () => { leaveTableFocus(); + setIsDraggingAndSelecting(true); }, }); @@ -132,11 +138,13 @@ export const EntityTable = ({ updateEntityMutation }: OwnProps) => {
- + {isDraggingAndSelecting && ( + + )} diff --git a/front/src/modules/ui/table/hooks/useTableColumns.ts b/front/src/modules/ui/table/hooks/useTableColumns.ts index 89eafd977..c0e0ac850 100644 --- a/front/src/modules/ui/table/hooks/useTableColumns.ts +++ b/front/src/modules/ui/table/hooks/useTableColumns.ts @@ -19,6 +19,19 @@ export const useTableColumns = () => { TableRecoilScopeContext, ); + const handleColumnReorder = useCallback( + (columns: ColumnDefinition[]) => { + const updatedColumnOrder = columns + .map((column, index) => { + return { ...column, index }; + }) + .sort((columnA, columnB) => columnA.index - columnB.index); + + setTableColumns(updatedColumnOrder); + }, + [setTableColumns], + ); + const handleColumnVisibilityChange = useCallback( (column: ColumnDefinition) => { const nextColumns = tableColumnsByKey[column.key] @@ -84,5 +97,6 @@ export const useTableColumns = () => { handleColumnVisibilityChange, handleColumnLeftMove, handleColumnRightMove, + handleColumnReorder, }; }; diff --git a/front/src/modules/ui/table/options/components/TableOptionsDropdownButton.tsx b/front/src/modules/ui/table/options/components/TableOptionsDropdownButton.tsx index 6e1c636d2..ab8e819a0 100644 --- a/front/src/modules/ui/table/options/components/TableOptionsDropdownButton.tsx +++ b/front/src/modules/ui/table/options/components/TableOptionsDropdownButton.tsx @@ -1,17 +1,27 @@ +import { useRecoilState } from 'recoil'; + import { StyledHeaderDropdownButton } from '@/ui/dropdown/components/StyledHeaderDropdownButton'; import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton'; - -import { TableOptionsDropdownId } from '../../constants/TableOptionsDropdownId'; +import { TableOptionsDropdownId } from '@/ui/table/constants/TableOptionsDropdownId'; +import { isDraggingAndSelectingState } from '@/ui/table/states/isDraggingAndSelectingState'; export const TableOptionsDropdownButton = () => { + const [, setIsDraggingAndSelecting] = useRecoilState( + isDraggingAndSelectingState, + ); const { isDropdownButtonOpen, toggleDropdownButton } = useDropdownButton({ dropdownId: TableOptionsDropdownId, }); + const toggleDropdown = () => { + setIsDraggingAndSelecting(false); + toggleDropdownButton(); + }; + return ( Options diff --git a/front/src/modules/ui/table/options/components/TableOptionsDropdownContent.tsx b/front/src/modules/ui/table/options/components/TableOptionsDropdownContent.tsx index 5a9eeee08..9a5bee91c 100644 --- a/front/src/modules/ui/table/options/components/TableOptionsDropdownContent.tsx +++ b/front/src/modules/ui/table/options/components/TableOptionsDropdownContent.tsx @@ -1,5 +1,6 @@ -import { useContext, useRef, useState } from 'react'; +import { useCallback, useContext, useRef, useState } from 'react'; import styled from '@emotion/styled'; +import { OnDragEndResponder } from '@hello-pangea/dnd'; import { useRecoilCallback, useRecoilValue, useResetRecoilState } from 'recoil'; import { Key } from 'ts-key-enum'; @@ -89,7 +90,8 @@ export const TableOptionsDropdownContent = () => { TableRecoilScopeContext, ); - const { handleColumnVisibilityChange } = useTableColumns(); + const { handleColumnVisibilityChange, handleColumnReorder } = + useTableColumns(); const { upsertView } = useUpsertView(); @@ -115,6 +117,21 @@ export const TableOptionsDropdownContent = () => { setCurrentMenu(option); }; + const handleReorderField: OnDragEndResponder = useCallback( + (result) => { + if (!result.destination) { + return; + } + + const reorderFields = Array.from(visibleTableColumns); + const [removed] = reorderFields.splice(result.source.index, 1); + reorderFields.splice(result.destination.index, 0, removed); + + handleColumnReorder(reorderFields); + }, + [visibleTableColumns, handleColumnReorder], + ); + const resetMenu = () => setCurrentMenu(undefined); useScopedHotkeys( @@ -186,6 +203,8 @@ export const TableOptionsDropdownContent = () => { title="Visible" fields={visibleTableColumns} onVisibilityChange={handleColumnVisibilityChange} + isDraggable={true} + onDragEnd={handleReorderField} /> {hiddenTableColumns.length > 0 && ( <> @@ -194,6 +213,7 @@ export const TableOptionsDropdownContent = () => { title="Hidden" fields={hiddenTableColumns} onVisibilityChange={handleColumnVisibilityChange} + isDraggable={false} /> )} diff --git a/front/src/modules/ui/table/states/isDraggingAndSelectingState.ts b/front/src/modules/ui/table/states/isDraggingAndSelectingState.ts new file mode 100644 index 000000000..f3677fa38 --- /dev/null +++ b/front/src/modules/ui/table/states/isDraggingAndSelectingState.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const isDraggingAndSelectingState = atom({ + key: 'isDraggingAndSelectingState', + default: true, +}); diff --git a/front/src/modules/ui/table/table-header/components/TableHeader.tsx b/front/src/modules/ui/table/table-header/components/TableHeader.tsx index b8e571066..f2d3d7e6d 100644 --- a/front/src/modules/ui/table/table-header/components/TableHeader.tsx +++ b/front/src/modules/ui/table/table-header/components/TableHeader.tsx @@ -12,6 +12,7 @@ import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScop import { TableOptionsDropdownId } from '../../constants/TableOptionsDropdownId'; import { TableOptionsDropdown } from '../../options/components/TableOptionsDropdown'; +import { isDraggingAndSelectingState } from '../../states/isDraggingAndSelectingState'; import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext'; import { savedTableColumnsFamilyState } from '../../states/savedTableColumnsFamilyState'; import { canPersistTableColumnsScopedFamilySelector } from '../../states/selectors/canPersistTableColumnsScopedFamilySelector'; @@ -40,6 +41,9 @@ export const TableHeader = () => { const [savedTableColumns, setSavedTableColumns] = useRecoilState( savedTableColumnsFamilyState(currentViewId), ); + const [, setIsDraggingAndSelecting] = useRecoilState( + isDraggingAndSelectingState, + ); const handleViewBarReset = () => setTableColumns(savedTableColumns); @@ -57,6 +61,7 @@ export const TableHeader = () => { const handleCurrentViewSubmit = async () => { if (canPersistTableColumns) { setSavedTableColumns(tableColumns); + setIsDraggingAndSelecting(true); } await onCurrentViewSubmit?.(); diff --git a/front/src/modules/ui/view-bar/components/ViewBarDetails.tsx b/front/src/modules/ui/view-bar/components/ViewBarDetails.tsx index daee5123e..6a80a1b27 100644 --- a/front/src/modules/ui/view-bar/components/ViewBarDetails.tsx +++ b/front/src/modules/ui/view-bar/components/ViewBarDetails.tsx @@ -1,8 +1,9 @@ import { type ReactNode, useContext } from 'react'; import styled from '@emotion/styled'; -import { useRecoilValue } from 'recoil'; +import { useRecoilState, useRecoilValue } from 'recoil'; import { IconArrowDown, IconArrowUp } from '@/ui/icon/index'; +import { isDraggingAndSelectingState } from '@/ui/table/states/isDraggingAndSelectingState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId'; @@ -115,6 +116,10 @@ export const ViewBarDetails = ({ ViewBarRecoilScopeContext, ); + const [, setIsDraggingAndSelecting] = useRecoilState( + isDraggingAndSelectingState, + ); + const savedFilters = useRecoilValue( savedFiltersFamilySelector(currentViewId), ); @@ -167,6 +172,7 @@ export const ViewBarDetails = ({ const handleCancelClick = () => { onViewBarReset?.(); + setIsDraggingAndSelecting(true); setFilters(savedFilters); setSorts(savedSorts); }; diff --git a/front/src/modules/ui/view-bar/components/ViewFieldsVisibilityDropdownSection.tsx b/front/src/modules/ui/view-bar/components/ViewFieldsVisibilityDropdownSection.tsx index bbfaabf06..707325a1b 100644 --- a/front/src/modules/ui/view-bar/components/ViewFieldsVisibilityDropdownSection.tsx +++ b/front/src/modules/ui/view-bar/components/ViewFieldsVisibilityDropdownSection.tsx @@ -1,3 +1,13 @@ +import styled from '@emotion/styled'; +import { + DragDropContext, + Draggable, + Droppable, + DropResult, + OnDragEndResponder, + ResponderProvided, +} from '@hello-pangea/dnd'; + import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer'; import { StyledDropdownMenuSubheader } from '@/ui/dropdown/components/StyledDropdownMenuSubheader'; import type { @@ -11,31 +21,86 @@ type OwnProps = { fields: Field[]; onVisibilityChange: (field: Field) => void; title: string; + isDraggable: boolean; + onDragEnd?: OnDragEndResponder; }; +const StyledDropdownMenuItemWrapper = styled.div` + width: 100%; +`; + export const ViewFieldsVisibilityDropdownSection = < Field extends ViewFieldDefinition, >({ fields, onVisibilityChange, title, -}: OwnProps) => ( - <> - {title} - - {fields.map((field) => ( - onVisibilityChange(field), - }, - ]} - text={field.name} - /> - ))} - - -); + isDraggable, + onDragEnd, +}: OwnProps) => { + const handleOnDrag = (result: DropResult, provided: ResponderProvided) => { + onDragEnd?.(result, provided); + }; + return ( + <> + {title} + + {isDraggable && ( + + + + {(provided) => ( +
+ {fields.map((field, index) => ( + + {(provided) => ( +
+ onVisibilityChange(field), + }, + ]} + text={field.name} + /> +
+ )} +
+ ))} + {provided.placeholder} +
+ )} +
+
+
+ )} + {!isDraggable && + fields.map((field) => ( + onVisibilityChange(field), + }, + ]} + text={field.name} + /> + ))} +
+ + ); +}; diff --git a/packages/eslint-plugin-twenty-ts/dist/index.js b/packages/eslint-plugin-twenty-ts/dist/index.js new file mode 100644 index 000000000..65510895f --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/dist/index.js @@ -0,0 +1,11 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +module.exports = { + rules: { + "effect-components": require("./src/rules/effect-components"), + "no-hardcoded-colors": require("./src/rules/no-hardcoded-colors"), + "matching-state-variable": require("./src/rules/matching-state-variable"), + "sort-css-properties-alphabetically": require("./src/rules/sort-css-properties-alphabetically"), + "styled-components-prefixed-with-styled": require("./src/rules/styled-components-prefixed-with-styled"), + }, +}; diff --git a/packages/eslint-plugin-twenty-ts/dist/src/rules/effect-components.js b/packages/eslint-plugin-twenty-ts/dist/src/rules/effect-components.js new file mode 100644 index 000000000..67c189e61 --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/dist/src/rules/effect-components.js @@ -0,0 +1,96 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("@typescript-eslint/utils"); +const createRule = utils_1.ESLintUtils.RuleCreator((name) => `https://docs.twenty.com`); +const checkIsPascalCase = (input) => { + const pascalCaseRegex = /^(?:\p{Uppercase_Letter}\p{Letter}*)+$/u; + return pascalCaseRegex.test(input); +}; +const effectComponentsRule = createRule({ + create(context) { + const checkThatNodeIsEffectComponent = (node) => { + var _a, _b, _c, _d, _e; + const isPascalCase = checkIsPascalCase((_b = (_a = node.id) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : ""); + if (!isPascalCase) { + return; + } + const isReturningFragmentOrNull = ( + // Direct return of JSX fragment, e.g., () => <> + (node.body.type === 'JSXFragment' && node.body.children.length === 0) || + // Direct return of null, e.g., () => null + (node.body.type === 'Literal' && node.body.value === null) || + // Return JSX fragment or null from block + (node.body.type === 'BlockStatement' && + node.body.body.some(statement => { + var _a, _b, _c; + return statement.type === 'ReturnStatement' && + ( + // Empty JSX fragment return, e.g., return <>; + (((_a = statement.argument) === null || _a === void 0 ? void 0 : _a.type) === 'JSXFragment' && statement.argument.children.length === 0) || + // Empty React.Fragment return, e.g., return ; + (((_b = statement.argument) === null || _b === void 0 ? void 0 : _b.type) === 'JSXElement' && + statement.argument.openingElement.name.type === 'JSXIdentifier' && + statement.argument.openingElement.name.name === 'React.Fragment' && + statement.argument.children.length === 0) || + // Literal null return, e.g., return null; + (((_c = statement.argument) === null || _c === void 0 ? void 0 : _c.type) === 'Literal' && statement.argument.value === null)); + }))); + const hasEffectSuffix = (_c = node.id) === null || _c === void 0 ? void 0 : _c.name.endsWith("Effect"); + const hasEffectSuffixButIsNotEffectComponent = hasEffectSuffix && !isReturningFragmentOrNull; + const isEffectComponentButDoesNotHaveEffectSuffix = !hasEffectSuffix && isReturningFragmentOrNull; + if (isEffectComponentButDoesNotHaveEffectSuffix) { + context.report({ + node, + messageId: "effectSuffix", + data: { + componentName: (_d = node.id) === null || _d === void 0 ? void 0 : _d.name, + }, + fix(fixer) { + var _a; + if (node.id) { + return fixer.replaceText(node.id, ((_a = node.id) === null || _a === void 0 ? void 0 : _a.name) + "Effect"); + } + return null; + }, + }); + } + else if (hasEffectSuffixButIsNotEffectComponent) { + context.report({ + node, + messageId: "noEffectSuffix", + data: { + componentName: (_e = node.id) === null || _e === void 0 ? void 0 : _e.name, + }, + fix(fixer) { + var _a; + if (node.id) { + return fixer.replaceText(node.id, (_a = node.id) === null || _a === void 0 ? void 0 : _a.name.replace("Effect", "")); + } + return null; + }, + }); + } + }; + return { + ArrowFunctionExpression: checkThatNodeIsEffectComponent, + FunctionDeclaration: checkThatNodeIsEffectComponent, + FunctionExpression: checkThatNodeIsEffectComponent, + }; + }, + name: "effect-components", + meta: { + docs: { + description: "Effect components should end with the Effect suffix. This rule checks only components that are in PascalCase and that return a JSX fragment or null. Any renderProps or camelCase components are ignored.", + }, + messages: { + effectSuffix: "Effect component {{ componentName }} should end with the Effect suffix.", + noEffectSuffix: "Component {{ componentName }} shouldn't end with the Effect suffix because it doesn't return a JSX fragment or null.", + }, + type: "suggestion", + schema: [], + fixable: "code", + }, + defaultOptions: [], +}); +module.exports = effectComponentsRule; +exports.default = effectComponentsRule; diff --git a/packages/eslint-plugin-twenty-ts/dist/src/rules/matching-state-variable.js b/packages/eslint-plugin-twenty-ts/dist/src/rules/matching-state-variable.js new file mode 100644 index 000000000..8e065d1f2 --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/dist/src/rules/matching-state-variable.js @@ -0,0 +1,113 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("@typescript-eslint/utils"); +const createRule = utils_1.ESLintUtils.RuleCreator((name) => `https://docs.twenty.com`); +const matchingStateVariableRule = createRule({ + create: (context) => { + return { + VariableDeclarator(node) { + var _a, _b, _c, _d, _e, _f, _g; + if (((_a = node === null || node === void 0 ? void 0 : node.init) === null || _a === void 0 ? void 0 : _a.type) === utils_1.AST_NODE_TYPES.CallExpression && + node.init.callee.type === utils_1.AST_NODE_TYPES.Identifier && + [ + "useRecoilState", + "useRecoilFamilyState", + "useRecoilSelector", + "useRecoilScopedState", + "useRecoilScopedFamilyState", + "useRecoilScopedSelector", + "useRecoilValue", + ].includes(node.init.callee.name)) { + const stateNameBase = ((_c = (_b = node.init.arguments) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.type) === utils_1.AST_NODE_TYPES.Identifier + ? node.init.arguments[0].name + : undefined; + if (!stateNameBase) { + return; + } + let expectedVariableNameBase = stateNameBase.replace(/(State|FamilyState|Selector|ScopedState|ScopedFamilyState|ScopedSelector)$/, ""); + if (node.id.type === utils_1.AST_NODE_TYPES.Identifier) { + const actualVariableName = node.id.name; + if (actualVariableName !== expectedVariableNameBase) { + context.report({ + node, + messageId: "invalidVariableName", + data: { + actual: actualVariableName, + expected: expectedVariableNameBase, + callee: node.init.callee.name, + }, + fix(fixer) { + return fixer.replaceText(node.id, expectedVariableNameBase); + }, + }); + } + return; + } + if (node.id.type === utils_1.AST_NODE_TYPES.ArrayPattern) { + const actualVariableName = ((_e = (_d = node.id.elements) === null || _d === void 0 ? void 0 : _d[0]) === null || _e === void 0 ? void 0 : _e.type) === utils_1.AST_NODE_TYPES.Identifier + ? node.id.elements[0].name + : undefined; + if (actualVariableName && + actualVariableName !== expectedVariableNameBase) { + context.report({ + node, + messageId: "invalidVariableName", + data: { + actual: actualVariableName, + expected: expectedVariableNameBase, + callee: node.init.callee.name, + }, + fix(fixer) { + if (node.id.type === utils_1.AST_NODE_TYPES.ArrayPattern) { + return fixer.replaceText(node.id.elements[0], expectedVariableNameBase); + } + return null; + }, + }); + return; + } + if (((_g = (_f = node.id.elements) === null || _f === void 0 ? void 0 : _f[1]) === null || _g === void 0 ? void 0 : _g.type) === utils_1.AST_NODE_TYPES.Identifier) { + const actualSetterName = node.id.elements[1].name; + const expectedSetterName = `set${expectedVariableNameBase + .charAt(0) + .toUpperCase()}${expectedVariableNameBase.slice(1)}`; + if (actualSetterName !== expectedSetterName) { + context.report({ + node, + messageId: "invalidSetterName", + data: { + actual: actualSetterName, + expected: expectedSetterName, + }, + fix(fixer) { + if (node.id.type === utils_1.AST_NODE_TYPES.ArrayPattern) { + return fixer.replaceText(node.id.elements[1], expectedSetterName); + } + return null; + }, + }); + } + } + } + } + }, + }; + }, + name: "recoil-hook-naming", + meta: { + type: "problem", + docs: { + description: "Ensure recoil value and setter are named after their atom name", + recommended: "recommended", + }, + fixable: "code", + schema: [], + messages: { + invalidVariableName: "Invalid usage of {{hookName}}: the value should be named '{{expectedName}}' but found '{{actualName}}'.", + invalidSetterName: "Invalid usage of {{hookName}}: Expected setter '{{expectedName}}' but found '{{actualName}}'.", + }, + }, + defaultOptions: [], +}); +module.exports = matchingStateVariableRule; +exports.default = matchingStateVariableRule; diff --git a/packages/eslint-plugin-twenty-ts/dist/src/rules/no-hardcoded-colors.js b/packages/eslint-plugin-twenty-ts/dist/src/rules/no-hardcoded-colors.js new file mode 100644 index 000000000..545d11189 --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/dist/src/rules/no-hardcoded-colors.js @@ -0,0 +1,42 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("@typescript-eslint/utils"); +const createRule = utils_1.ESLintUtils.RuleCreator((name) => `https://docs.twenty.com`); +const noHardcodedColorsRule = createRule({ + create(context) { + return { + TaggedTemplateExpression(node) { + if (context.getFilename().endsWith("themes.ts")) { + return; + } + node.quasi.quasis.forEach((quasi) => { + const colorRegex = /(?:rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(,\s*\d+\.?\d*)?\))|(?:#[0-9a-fA-F]{6})/i; + if (colorRegex.test(quasi.value.raw)) { + context.report({ + node, + messageId: "hardcodedColor", + data: { + color: quasi.value.raw, + }, + }); + } + }); + }, + }; + }, + name: "no-hardcoded-colors", + meta: { + docs: { + description: "Do not use hardcoded RGBA or Hex colors. Please use a color from the theme file.", + }, + messages: { + hardcodedColor: "Hardcoded color {{ color }} found. Please use a color from the theme file.", + }, + type: "suggestion", + schema: [], + fixable: "code", + }, + defaultOptions: [], +}); +module.exports = noHardcodedColorsRule; +exports.default = noHardcodedColorsRule; diff --git a/packages/eslint-plugin-twenty-ts/dist/src/rules/sort-css-properties-alphabetically.js b/packages/eslint-plugin-twenty-ts/dist/src/rules/sort-css-properties-alphabetically.js new file mode 100644 index 000000000..3733faa7d --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/dist/src/rules/sort-css-properties-alphabetically.js @@ -0,0 +1,182 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const postcss_1 = __importDefault(require("postcss")); +const utils_1 = require("@typescript-eslint/utils"); +const createRule = utils_1.ESLintUtils.RuleCreator((name) => `https://docs.twenty.com`); +const isStyledTagname = (node) => { + return ((node.tag.type === "Identifier" && node.tag.name === "css") || + (node.tag.type === "MemberExpression" && + // @ts-ignore + node.tag.object.name === "styled") || + (node.tag.type === "CallExpression" && + // @ts-ignore + (node.tag.callee.name === "styled" || + // @ts-ignore + (node.tag.callee.object && + // @ts-ignore + ((node.tag.callee.object.callee && + // @ts-ignore + node.tag.callee.object.callee.name === "styled") || + // @ts-ignore + (node.tag.callee.object.object && + // @ts-ignore + node.tag.callee.object.object.name === "styled")))))); +}; +/** + * An atomic rule is a rule without nested rules. + */ +const isValidAtomicRule = (rule) => { + const decls = rule.nodes.filter((node) => node.type === "decl"); + if (decls.length < 0) { + return { isValid: true }; + } + for (let i = 1; i < decls.length; i++) { + const current = decls[i].prop; + const prev = decls[i - 1].prop; + if (current < prev) { + const loc = { + start: { + line: decls[i - 1].source.start.line, + column: decls[i - 1].source.start.column - 1, + }, + end: { + line: decls[i].source.end.line, + column: decls[i].source.end.column - 1, + }, + }; + return { isValid: false, loc }; + } + } + return { isValid: true }; +}; +const isValidRule = (rule) => { + // check each rule recursively + const { isValid, loc } = rule.nodes.reduce((map, node) => { + return node.type === "rule" + ? isValidRule(node) + : map; + }, { isValid: true }); + // if there is any invalid rule, return result + if (!isValid) { + return { isValid, loc }; + } + // check declarations + return isValidAtomicRule(rule); +}; +const getNodeStyles = (node) => { + const [firstQuasi, ...quasis] = node.quasi.quasis; + // remove line break added to the first quasi + const lineBreakCount = node.quasi.loc.start.line - 1; + let styles = `${"\n".repeat(lineBreakCount)}${" ".repeat(node.quasi.loc.start.column + 1)}${firstQuasi.value.raw}`; + // replace expression by spaces and line breaks + quasis.forEach(({ value, loc }, idx) => { + const prevLoc = idx === 0 ? firstQuasi.loc : quasis[idx - 1].loc; + const lineBreaksCount = loc.start.line - prevLoc.end.line; + const spacesCount = loc.start.line === prevLoc.end.line + ? loc.start.column - prevLoc.end.column + 2 + : loc.start.column + 1; + styles = `${styles}${" "}${"\n".repeat(lineBreaksCount)}${" ".repeat(spacesCount)}${value.raw}`; + }); + return styles; +}; +const fix = ({ rule, fixer, src, }) => { + let fixings = []; + // concat fixings recursively + rule.nodes.forEach((node) => { + if (node.type === "rule") { + fixings = [...fixings, ...fix({ rule: node, fixer, src })]; + } + }); + const declarations = rule.nodes.filter((node) => node.type === "decl"); + const sortedDeclarations = sortDeclarations(declarations); + declarations.forEach((decl, idx) => { + if (!areSameDeclarations(decl, sortedDeclarations[idx])) { + try { + const range = getDeclRange({ decl, src }); + const sortedDeclText = getDeclText({ + decl: sortedDeclarations[idx], + src, + }); + fixings.push(fixer.removeRange([range.startIdx, range.endIdx + 1])); + fixings.push(fixer.insertTextAfterRange([range.startIdx, range.startIdx], sortedDeclText)); + } + catch (e) { + console.log(e); + } + } + }); + return fixings; +}; +const areSameDeclarations = (a, b) => a.source.start.line === b.source.start.line && + a.source.start.column === b.source.start.column; +const getDeclRange = ({ decl, src, }) => { + const loc = { + start: { + line: decl.source.start.line, + column: decl.source.start.column - 1, + }, + end: { + line: decl.source.end.line, + column: decl.source.end.column - 1, + }, + }; + const startIdx = src.getIndexFromLoc(loc.start); + const endIdx = src.getIndexFromLoc(loc.end); + return { startIdx, endIdx }; +}; +const getDeclText = ({ decl, src, }) => { + const { startIdx, endIdx } = getDeclRange({ decl, src }); + return src.getText().substring(startIdx, endIdx + 1); +}; +const sortDeclarations = (declarations) => declarations + .slice() + .sort((declA, declB) => (declA.prop > declB.prop ? 1 : -1)); +const sortCssPropertiesAlphabeticallyRule = createRule({ + create(context) { + return { + TaggedTemplateExpression(node) { + if (isStyledTagname(node)) { + try { + const root = postcss_1.default.parse(getNodeStyles(node)); + const { isValid, loc } = isValidRule(root); + if (!isValid) { + return context.report({ + node, + messageId: "sort-css-properties-alphabetically", + loc, + fix: (fixer) => fix({ + // @ts-ignore + rule: root, + fixer, + src: context.getSourceCode(), + }), + }); + } + } + catch (e) { + return true; + } + } + }, + }; + }, + name: "sort-css-properties-alphabetically", + meta: { + docs: { + description: "Styles are sorted alphabetically.", + recommended: "recommended", + }, + messages: { + "sort-css-properties-alphabetically": "Declarations should be sorted alphabetically.", + }, + type: "suggestion", + schema: [], + fixable: "code", + }, + defaultOptions: [], +}); +module.exports = sortCssPropertiesAlphabeticallyRule; +exports.default = sortCssPropertiesAlphabeticallyRule; diff --git a/packages/eslint-plugin-twenty-ts/dist/src/rules/styled-components-prefixed-with-styled.js b/packages/eslint-plugin-twenty-ts/dist/src/rules/styled-components-prefixed-with-styled.js new file mode 100644 index 000000000..8cf2a8f52 --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/dist/src/rules/styled-components-prefixed-with-styled.js @@ -0,0 +1,49 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("@typescript-eslint/utils"); +const createRule = utils_1.ESLintUtils.RuleCreator((name) => `https://docs.twenty.com`); +const styledComponentsPrefixedWithStyledRule = createRule({ + create(context) { + return { + VariableDeclarator: (node) => { + const templateExpr = node.init; + if ((templateExpr === null || templateExpr === void 0 ? void 0 : templateExpr.type) !== utils_1.AST_NODE_TYPES.TaggedTemplateExpression) { + return; + } + const tag = templateExpr.tag; + const tagged = tag.type === utils_1.AST_NODE_TYPES.MemberExpression ? tag.object + : tag.type === utils_1.AST_NODE_TYPES.CallExpression ? tag.callee + : null; + if ((tagged === null || tagged === void 0 ? void 0 : tagged.type) === utils_1.AST_NODE_TYPES.Identifier && tagged.name === 'styled') { + const variable = node.id; + if (variable.name.startsWith('Styled')) { + return; + } + context.report({ + node, + messageId: 'noStyledPrefix', + data: { + componentName: variable.name + } + }); + } + }, + }; + }, + name: 'styled-components-prefixed-with-styled', + meta: { + type: 'suggestion', + docs: { + description: 'Warn when StyledComponents are not prefixed with Styled', + recommended: "recommended" + }, + messages: { + noStyledPrefix: '{{componentName}} is a StyledComponent and is not prefixed with Styled.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [] +}); +module.exports = styledComponentsPrefixedWithStyledRule; +exports.default = styledComponentsPrefixedWithStyledRule; diff --git a/packages/eslint-plugin-twenty-ts/dist/src/tests/effect-components.spec.js b/packages/eslint-plugin-twenty-ts/dist/src/tests/effect-components.spec.js new file mode 100644 index 000000000..922f6c9d8 --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/dist/src/tests/effect-components.spec.js @@ -0,0 +1,87 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const rule_tester_1 = require("@typescript-eslint/rule-tester"); +const effect_components_1 = __importDefault(require("../rules/effect-components")); +const ruleTester = new rule_tester_1.RuleTester({ + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + }, +}); +ruleTester.run("effect-components", effect_components_1.default, { + valid: [ + { + code: `const TestComponentEffect = () => <>;`, + }, + { + code: `const TestComponent = () =>
;`, + }, + { + code: `export const useUpdateEffect = () => null;`, + }, + { + code: `export const useUpdateEffect = () => <>;`, + }, + { + code: `const TestComponent = () => <>
;`, + }, + { + code: `const TestComponentEffect = () => null;`, + }, + { + code: `const TestComponentEffect = () => { + useEffect(() => {}, []); + + return null; + }`, + }, + { + code: `const TestComponentEffect = () => { + useEffect(() => {}, []); + + return <>; + }`, + }, + { + code: `const TestComponentEffect = () => { + useEffect(() => {}, []); + + return <>; + }`, + }, + { + code: `const TestComponentEffect = () => { + useEffect(() => {}, []); + + return null; + }`, + }, + ], + invalid: [ + { + code: "const TestComponent = () => <>;", + output: 'const TestComponentEffect = () => <>;', + errors: [ + { + messageId: "effectSuffix", + }, + ], + }, + { + code: "const TestComponentEffect = () => <>
;", + output: 'const TestComponent = () => <>
;', + errors: [ + { + messageId: "noEffectSuffix", + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin-twenty-ts/dist/src/tests/file.js b/packages/eslint-plugin-twenty-ts/dist/src/tests/file.js new file mode 100644 index 000000000..603e5d606 --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/dist/src/tests/file.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +// Required by typescript-eslint https://typescript-eslint.io/packages/rule-tester#type-aware-testing diff --git a/packages/eslint-plugin-twenty-ts/dist/src/tests/matching-state-variable.spec.js b/packages/eslint-plugin-twenty-ts/dist/src/tests/matching-state-variable.spec.js new file mode 100644 index 000000000..dfe4adfc3 --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/dist/src/tests/matching-state-variable.spec.js @@ -0,0 +1,50 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const rule_tester_1 = require("@typescript-eslint/rule-tester"); +const matching_state_variable_1 = __importDefault(require("../rules/matching-state-variable")); +const ruleTester = new rule_tester_1.RuleTester({ + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + }, +}); +ruleTester.run('matching-state-variable', matching_state_variable_1.default, { + valid: [ + { + code: 'const variable = useRecoilValue(variableState);', + }, + { + code: 'const [variable, setVariable] = useRecoilState(variableState);', + }, + ], + invalid: [ + { + code: 'const myValue = useRecoilValue(variableState);', + errors: [ + { + messageId: 'invalidVariableName', + }, + ], + output: 'const variable = useRecoilValue(variableState);', + }, + { + code: 'const [myValue, setMyValue] = useRecoilState(variableState);', + errors: [ + { + messageId: 'invalidVariableName', + }, + { + messageId: 'invalidSetterName', + }, + ], + output: 'const [variable, setVariable] = useRecoilState(variableState);', + }, + ], +}); diff --git a/packages/eslint-plugin-twenty-ts/dist/src/tests/no-hardcoded-colors.spec.js b/packages/eslint-plugin-twenty-ts/dist/src/tests/no-hardcoded-colors.spec.js new file mode 100644 index 000000000..27315b711 --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/dist/src/tests/no-hardcoded-colors.spec.js @@ -0,0 +1,45 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const rule_tester_1 = require("@typescript-eslint/rule-tester"); +const no_hardcoded_colors_1 = __importDefault(require("../rules/no-hardcoded-colors")); +const ruleTester = new rule_tester_1.RuleTester({ + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + }, +}); +ruleTester.run("no-hardcoded-colors", no_hardcoded_colors_1.default, { + valid: [ + { + code: "const color = theme.background.secondary;", + }, + { + code: 'const color = "#000000";', + }, + ], + invalid: [ + { + code: 'const color = "rgb(154,205,50)";', + errors: [ + { + messageId: "hardcodedColor", + }, + ], + }, + { + code: 'const color = "#ADFF2F";', + errors: [ + { + messageId: "hardcodedColor", + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin-twenty-ts/dist/src/tests/react.js b/packages/eslint-plugin-twenty-ts/dist/src/tests/react.js new file mode 100644 index 000000000..603e5d606 --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/dist/src/tests/react.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +// Required by typescript-eslint https://typescript-eslint.io/packages/rule-tester#type-aware-testing diff --git a/packages/eslint-plugin-twenty-ts/dist/src/tests/sort-css-properties-alphabetically.spec.js b/packages/eslint-plugin-twenty-ts/dist/src/tests/sort-css-properties-alphabetically.spec.js new file mode 100644 index 000000000..256110ca8 --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/dist/src/tests/sort-css-properties-alphabetically.spec.js @@ -0,0 +1,61 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const rule_tester_1 = require("@typescript-eslint/rule-tester"); +const sort_css_properties_alphabetically_1 = __importDefault(require("../rules/sort-css-properties-alphabetically")); +const ruleTester = new rule_tester_1.RuleTester({ + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + }, +}); +ruleTester.run("sort-css-properties-alphabetically", sort_css_properties_alphabetically_1.default, { + valid: [ + { + code: 'const style = css`color: red;`;', + filename: 'react.tsx', + }, + { + code: 'const style = styled.div`background-color: $bgColor;`;', + filename: 'react.tsx', + }, + ], + invalid: [ + { + code: 'const style = css`color: #FF0000;`;', + filename: 'react.tsx', + errors: [ + { + messageId: "sort-css-properties-alphabetically", + suggestions: [ + { + messageId: "sort-css-properties-alphabetically", + output: 'const style = css`color: red;`;', + }, + ], + }, + ], + }, + { + code: 'const style = styled.div`background-color: $bgColor; color: #FFFFFF;`;', + filename: 'react.tsx', + errors: [ + { + messageId: "sort-css-properties-alphabetically", + suggestions: [ + { + messageId: "sort-css-properties-alphabetically", + output: 'const style = styled.div`background-color: $bgColor; color: white;`;', + }, + ], + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin-twenty-ts/dist/src/tests/styled-components-prefixed-with-styled.spec.js b/packages/eslint-plugin-twenty-ts/dist/src/tests/styled-components-prefixed-with-styled.spec.js new file mode 100644 index 000000000..eb0e8e1ba --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/dist/src/tests/styled-components-prefixed-with-styled.spec.js @@ -0,0 +1,49 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const rule_tester_1 = require("@typescript-eslint/rule-tester"); +const styled_components_prefixed_with_styled_1 = __importDefault(require("../rules/styled-components-prefixed-with-styled")); +const ruleTester = new rule_tester_1.RuleTester({ + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + }, +}); +ruleTester.run("styled-components-prefixed-with-styled", styled_components_prefixed_with_styled_1.default, { + valid: [ + { + code: 'const StyledButton = styled.button``;', + filename: 'react.tsx', + }, + { + code: 'const StyledComponent = styled.div``;', + filename: 'react.tsx', + }, + ], + invalid: [ + { + code: 'const Button = styled.button``;', + filename: 'react.tsx', + errors: [ + { + messageId: 'noStyledPrefix', + }, + ], + }, + { + code: 'const Component = styled.div``;', + filename: 'react.tsx', + errors: [ + { + messageId: 'noStyledPrefix', + }, + ], + }, + ], +});