2aeacf341f
## Summary - Flatten system object entries into the first-level workflow object pickers. - Flatten dashboard Source and record-page Field picker advanced/system entries into the main searchable list. - Keep regular entries first, place system/advanced entries at the bottom, and cap page-layout picker height at 340px. - Add keyboard selection support to workflow object pickers through `SelectableList`. ## Review notes - Removed now-unused Advanced submenu state, submenu headers, and duplicated filtering paths. - Kept the width behavior scoped to each existing dropdown; the shared page-layout wrapper only controls height/scrolling. - No blocking issues found in the final reviewed diff. ## Screenshots ### Workflow record type picker | Before | After | | --- | --- | | <img src="https://gist.githubusercontent.com/Bonapara/d2b6754b3f1927b3d755b5f260e8b12f/raw/dc3f7ef933bdca6c39599c45c827db0f27f01e7d/before-workflow-record-type-advanced.png" width="320" /> | <img src="https://gist.githubusercontent.com/Bonapara/d2b6754b3f1927b3d755b5f260e8b12f/raw/2ee195a8562e13d0f4307e7891ab60cfaecae8cf/after-workflow-record-type-flat.png" width="320" /> | ### Dashboard Source picker | Before | After | | --- | --- | | <img src="https://gist.githubusercontent.com/Bonapara/d2b6754b3f1927b3d755b5f260e8b12f/raw/45f57728ec7643c7048e857829e977d49b9b365c/before-dashboard-source-tall.png" width="420" /> | <img src="https://gist.githubusercontent.com/Bonapara/d2b6754b3f1927b3d755b5f260e8b12f/raw/f8b864ea5e3d04a00b9b6a613e8bf97dd545933c/after-dashboard-source-340px.png" width="420" /> | ### Record page Field picker | Before | After | | --- | --- | | <img src="https://gist.githubusercontent.com/Bonapara/d2b6754b3f1927b3d755b5f260e8b12f/raw/ea5f14ccac6c0d9b3d953b4047dc7491702f756a/before-record-field-advanced.png" width="320" /> | <img src="https://gist.githubusercontent.com/Bonapara/d2b6754b3f1927b3d755b5f260e8b12f/raw/9d0717cf3cfcb47ed17d595ba83b19a37718c3eb/after-record-field-flat.png" width="320" /> | ## Checks - `npx oxfmt --check` on touched files - `git diff --check` - `npx tsc -p tsconfig.json --noEmit --pretty false --noErrorTruncation | rg "(ChartDataSourceDropdownContent|FieldWidgetFieldDropdownContent|PageLayoutDropdownContentContainer|WorkflowObjectDropdownContent|WorkflowEditTriggerDatabaseEventForm|WorkflowEditActionFindRecords|WorkflowEditActionPickRecord|ChartSettingItem)"` returned no touched-file diagnostics - Browser verification: dashboard Source top/bottom, record-page Field top/bottom, workflow Record Type top/bottom, and ArrowDown selection in workflow Record Type menu <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22161?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
205 lines
5.9 KiB
Plaintext
205 lines
5.9 KiB
Plaintext
---
|
|
description: Code style guidelines for Twenty CRM
|
|
globs: []
|
|
alwaysApply: true
|
|
---
|
|
# Code Style Guidelines
|
|
|
|
## Formatting Standards
|
|
- **Prettier**: 2-space indentation, single quotes, trailing commas, semicolons
|
|
- **Print width**: 80 characters
|
|
- **Oxlint**: No unused imports, consistent import ordering, prefer const over let
|
|
|
|
## Naming Conventions
|
|
```typescript
|
|
// ✅ Variables and functions - camelCase
|
|
const userAccountBalance = 1000;
|
|
const calculateMonthlyPayment = () => {};
|
|
|
|
// ✅ Constants - SCREAMING_SNAKE_CASE
|
|
const API_ENDPOINTS = {
|
|
USERS: '/api/users',
|
|
ORDERS: '/api/orders',
|
|
} as const;
|
|
|
|
// ✅ Types and Classes - PascalCase
|
|
class UserService {}
|
|
type UserAccountData = {};
|
|
type ButtonProps = {}; // Component props suffix with 'Props'
|
|
|
|
// ✅ Files and directories - kebab-case
|
|
// user-profile.component.tsx
|
|
// user-profile.styles.ts
|
|
|
|
// ❌ NEVER use abbreviations in variable names
|
|
// Bad
|
|
const users = data.map((u) => u.name);
|
|
const field = items.find((f) => f.id === id);
|
|
|
|
// Good
|
|
const users = data.map((user) => user.name);
|
|
const field = items.find((item) => item.id === id);
|
|
const fieldMetadata = inlineFields.find(
|
|
(fieldMetadataItem) => fieldMetadataItem.name === fieldName,
|
|
);
|
|
```
|
|
|
|
## Import Organization
|
|
```typescript
|
|
// ✅ Correct import order
|
|
// 1. External libraries
|
|
import React from 'react';
|
|
import { useCallback } from 'react';
|
|
import styled from 'styled-components';
|
|
|
|
// 2. Internal modules (absolute paths)
|
|
import { Button } from '@/components/ui';
|
|
import { UserService } from '@/services';
|
|
|
|
// 3. Relative imports
|
|
import { UserCardProps } from './types';
|
|
```
|
|
|
|
## Function Structure
|
|
```typescript
|
|
// ✅ Small, focused functions
|
|
// ✅ Required parameters first, optional last
|
|
const processUserData = (
|
|
user: User,
|
|
options: ProcessingOptions,
|
|
callback?: (result: ProcessedUser) => void
|
|
): ProcessedUser => {
|
|
const processedUser = transformUserData(user);
|
|
applyOptions(processedUser, options);
|
|
|
|
if (callback) {
|
|
callback(processedUser);
|
|
}
|
|
|
|
return processedUser;
|
|
};
|
|
```
|
|
|
|
## Collection Transformations
|
|
```typescript
|
|
// ✅ Prefer array transformations for simple mapping, filtering, and classification
|
|
const activeUsers = users.filter((user) => user.isActive === true);
|
|
const userNames = users.map((user) => user.name);
|
|
|
|
const { payingUsers, nonPayingUsers } = users.reduce<{
|
|
payingUsers: User[];
|
|
nonPayingUsers: User[];
|
|
}>(
|
|
(accumulator, user) => {
|
|
if (user.isPaying === true) {
|
|
accumulator.payingUsers.push(user);
|
|
} else {
|
|
accumulator.nonPayingUsers.push(user);
|
|
}
|
|
|
|
return accumulator;
|
|
},
|
|
{ payingUsers: [], nonPayingUsers: [] },
|
|
);
|
|
|
|
// ❌ Avoid manual loops when map, filter, or reduce keeps the code clear
|
|
const payingUsers: User[] = [];
|
|
|
|
for (const user of users) {
|
|
if (user.isPaying === true) {
|
|
payingUsers.push(user);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Comments
|
|
```typescript
|
|
// ✅ Use short-form comments, NOT JSDoc blocks
|
|
// ✅ Explain business logic and non-obvious intentions (WHY, not WHAT)
|
|
// Apply 15% discount for premium users with orders > $100
|
|
const discount = isPremiumUser && orderTotal > 100 ? 0.15 : 0;
|
|
|
|
// TODO: Replace with proper authentication service
|
|
const isAuthenticated = localStorage.getItem('token') !== null;
|
|
|
|
// ✅ Multi-line comments use multiple // lines (NOT /** */ blocks)
|
|
// Calculates the total price after applying tax and discount
|
|
// Returns the final price that should be charged to the customer
|
|
const calculateTotalPrice = (basePrice: number): number => {
|
|
// Implementation
|
|
};
|
|
|
|
// ❌ AVOID obvious comments that just describe what code does
|
|
// Bad: Get all inline fields dynamically
|
|
const { inlineFieldMetadataItems } = useFieldListFieldMetadataItems({...});
|
|
|
|
// Bad: Define standard fields in display order
|
|
const standardFieldOrder = ['startsAt', 'endsAt', 'conferenceLink'];
|
|
|
|
// Bad: Split fields into standard and custom
|
|
const standardFields = standardFieldOrder.map(...)
|
|
|
|
// ✅ GOOD: Only comment if explaining non-obvious business logic
|
|
// Calendar events display standard fields first, then custom fields after participants
|
|
// to maintain consistency with the legacy UI behavior
|
|
const standardFields = standardFieldOrder.map(...)
|
|
|
|
// ❌ AVOID JSDoc blocks - use short comments instead
|
|
/**
|
|
* This style is NOT preferred in this codebase
|
|
*/
|
|
```
|
|
|
|
**Comment Guidelines:**
|
|
- **DO** comment complex business rules or domain-specific logic
|
|
- **DO** comment non-obvious algorithmic decisions
|
|
- **DO** add TODOs for future improvements
|
|
- **DON'T** comment obvious variable declarations or function calls
|
|
- **DON'T** comment what is already clear from well-named variables/functions
|
|
- **DON'T** add comments that just repeat what the code says
|
|
|
|
## Utility Helpers
|
|
```typescript
|
|
// ✅ Use existing utility helpers instead of manual checks
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import { isNonEmptyString, isNonEmptyArray } from '@sniptt/guards';
|
|
|
|
// ❌ Manual type guards
|
|
const validItems = items.filter((item): item is Item => item !== undefined);
|
|
const hasValue = value !== null && value !== undefined;
|
|
|
|
// ✅ Use utility helpers
|
|
const validItems = items.filter(isDefined);
|
|
const hasValue = isDefined(value);
|
|
|
|
// Other useful helpers:
|
|
// - isDefined(value) - checks !== null && !== undefined
|
|
// - isNonEmptyString(value) - checks string is defined and not empty
|
|
// - isNonEmptyArray(value) - checks array is defined and has items
|
|
```
|
|
|
|
## Security Patterns
|
|
```typescript
|
|
// ✅ CSV Export: Always apply security first, then formatting
|
|
const safeValue = formatValueForCSV(sanitizeValueForCSVExport(userInput));
|
|
|
|
// ✅ Input validation before processing
|
|
const sanitizedInput = validateAndSanitize(userInput);
|
|
const result = processData(sanitizedInput);
|
|
```
|
|
|
|
## Error Handling
|
|
```typescript
|
|
// ✅ Proper error types and meaningful messages
|
|
try {
|
|
const user = await userService.findById(userId);
|
|
if (!user) {
|
|
throw new UserNotFoundError(`User with ID ${userId} not found`);
|
|
}
|
|
return user;
|
|
} catch (error) {
|
|
logger.error('Failed to fetch user', { userId, error });
|
|
throw error;
|
|
}
|
|
```
|