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) => (
-
- >
-);
+ isDraggable,
+ onDragEnd,
+}: OwnProps) => {
+ const handleOnDrag = (result: DropResult, provided: ResponderProvided) => {
+ onDragEnd?.(result, provided);
+ };
+ return (
+ <>
+ {title}
+
+ {isDraggable && (
+
+
+
+ {(provided) => (
+
+ {fields.map((field, index) => (
+
+ {(provided) => (
+
+
+ )}
+
+ ))}
+ {provided.placeholder}
+
+ )}
+
+
+
+ )}
+ {!isDraggable &&
+ fields.map((field) => (
+
+ >
+ );
+};
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',
+ },
+ ],
+ },
+ ],
+});