This file provides context and guidelines for working with the ByteChef client codebase. It's automatically loaded by Cursor to maintain consistency across the team.
ByteChef is a workflow automation platform. The client is a React application built with:
- Framework: React with TypeScript
- Build Tool: Vite
- State Management: Zustand
- Data Fetching: TanStack Query (React Query)
- Routing: React Router DOM
- Forms: React Hook Form + Zod
- UI Components: Radix UI + shadcn/ui
- Styling: Tailwind CSS
- Testing: Vitest (unit/component) + Playwright (E2E)
- Internationalization: Lingui
- Code Generation: GraphQL Codegen for TypeScript + React Query hooks
- Workflow Editor: Built with
@xyflow/reactfor node-based workflow visualization - Rich Text Inputs: Built with TipTap (ProseMirror-based) for rich text editing
- Indentation: 4 spaces (not tabs)
- Quotes: Single quotes for strings
- Semicolons: Always use semicolons
- Trailing Commas: ES5 style (objects, arrays, function parameters)
- Bracket Spacing: NO spaces inside curly braces:
{foo}is correct,{ foo }is wrong - Line Width: Let Prettier handle it (default 80-100)
- Empty Lines Between Invocations: Add empty lines between function/method invocations for better readability
- Single-Statement Functions: Omit unnecessary curly braces in single-statement arrow functions
- No Abbreviations: Use full, descriptive names instead of abbreviations
- ❌
efor event → ✅event - ❌
errfor error → ✅error - ❌
resfor response → ✅response - ❌
reqfor request → ✅request - ❌
valfor value → ✅value - ❌
idxfor index → ✅index
- ❌
- Method Parameters: Use descriptive names derived from context, not single letters
- ❌
(a, b) => a + b→ ✅(firstNumber, secondNumber) => firstNumber + secondNumber - ❌
(acc, item) => {...}→ ✅(accumulator, item) => {...}or(result, item) => {...} - ❌
(e) => {...}→ ✅(event) => {...} - ❌
(err) => {...}→ ✅(error) => {...}
- ❌
- Context-Derived Names: Variable names should reflect their purpose and context
- In a reduce function:
accumulatororresultinstead ofacc - In a map function:
itemorelementinstead ofxore - In event handlers:
eventinstead ofe - In error handlers:
errorinstead oferr
- In a reduce function:
- Strict Mode: Always enabled
- Type Naming:
- Interfaces: PascalCase with suffix
IorProps(e.g.,AuthenticationI,ButtonProps) - Type Aliases: PascalCase with suffix
Type(e.g.,ButtonPropsType)
- Interfaces: PascalCase with suffix
- File Naming:
- Components: PascalCase (e.g.,
Button.tsx,Login.tsx) - Utilities/Hooks: camelCase (e.g.,
test-utils.tsx,useAnalytics.ts) - Tests: Same as source file with
.test.tsxor.spec.tssuffix
- Components: PascalCase (e.g.,
- Group imports in this order (enforced by ESLint):
- External libraries (React, third-party)
- Internal absolute imports with
@/alias - Relative imports
- Sort imports alphabetically within each group
- Sort destructured imports alphabetically
- Use
useShallowfromzustand/react/shallowwhen selecting multiple store values
- Custom ByteChef rules enforce:
- Empty line between JSX elements
- Import grouping and sorting
- No conditional object keys
- No duplicate imports
- Ref names must end with
Refsuffix - State variables must follow naming pattern
- TypeScript naming conventions for interfaces and types
- React hooks rules (exhaustive deps)
- Tailwind CSS class ordering
// 1. Imports (grouped and sorted)
import {useState} from 'react';
import {useAuthenticationStore} from '@/shared/stores/useAuthenticationStore';
// 2. Types/Interfaces
interface ComponentProps {
// props
}
// 3. Component (functional component with forwardRef if needed)
const Component = React.forwardRef<HTMLDivElement, ComponentProps>(({prop1, prop2}, ref) => {
// 4. Hooks
const {value} = useAuthenticationStore(
useShallow((state) => ({
value: state.value,
}))
);
// 5. State
const [localState, setLocalState] = useState();
// 6. Handlers
const handleClick = () => {};
// 7. Effects
useEffect(() => {}, []);
// 8. Render
return <div ref={ref}>Content</div>;
});
Component.displayName = 'Component';
export default Component;- Use functional components with hooks
- Use
React.forwardRefwhen component needs to forward refs - Always set
displayNamefor forwardRef components - Use
twMerge()utility for conditional class names - Prefer composition over configuration
- Extract complex logic into custom hooks
- Base UI components are in
src/components/ui/(shadcn/ui) - Custom components in
src/components/ - Use Radix UI primitives for accessible components
- Use
class-variance-authority(cva) for variant-based styling - Components should accept
classNameprop and merge withtwMerge()
- Built with
@xyflow/reactfor node-based workflow visualization - Location:
src/pages/platform/workflow-editor/ - Uses React Flow for drag-and-drop node editing
- Nodes represent workflow tasks, edges represent connections
- Custom node types and handles for workflow-specific functionality
- Built with TipTap (ProseMirror-based) for rich text editing
- Used for property inputs that require rich text formatting
- TipTap extensions: document, paragraph, text, mention, placeholder
- Custom mention system for property references
- Store location:
src/shared/stores/ - Pattern:
import {createStore, useStore} from 'zustand';
import {devtools} from 'zustand/middleware';
import {ExtractState} from 'zustand/vanilla';
export interface StoreI {
// state properties
// action methods
}
export const store = createStore<StoreI>()(
devtools(
(set, get) => ({
// initial state
// actions using set() and get()
}),
{name: 'store-name'}
)
);
export function useStore<U>(selector: (state: ExtractState<typeof store>) => U): U {
return useStore(store, selector);
}- Use
useShallowwhen selecting multiple values to prevent unnecessary re-renders (the selector returns a new object each time, so without a shallow compare the component would re-render on every store update):
const {value1, value2} = useStore(
useShallow((state) => ({
value1: state.value1,
value2: state.value2,
}))
);-
Do not use
useShallowfor single-value selectors. A selector that returns one slice (e.g.(state) => state.foo) is already stable; the component only re-renders when that slice changes.useShallowis only needed when the selector returns an object of multiple fields. -
Main stores:
useAuthenticationStore- User authentication stateuseEnvironmentStore- Environment selection (Development/Staging/Production)useApplicationInfoStore- Application configuration and infouseFeatureFlagsStore- Feature flag toggles
- GraphQL queries in
src/graphql/organized by domain - Generated hooks in
src/shared/middleware/(auto-generated, don't edit) - Use React Query hooks from generated code:
import {useProjectsQuery} from '@/shared/middleware/automation/configuration';
const {data, isLoading, error} = useProjectsQuery();- Mutations in
src/shared/mutations/organized by domain - Use React Query's
useMutationwith generated types:
import {useCreateProjectMutation} from '@/shared/mutations/automation/projects.mutations';
const mutation = useCreateProjectMutation({
onSuccess: (data) => {
// handle success
},
});
mutation.mutate({projectData});- Query keys defined in
src/shared/queries/organized by domain - Use consistent key structure for cache invalidation
- Define Zod schema first:
const formSchema = z.object({
email: z.string().email().min(5, 'Email is required'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});- Use with React Hook Form:
import {useForm} from 'react-hook-form';
import {zodResolver} from '@hookform/resolvers/zod';
const form = useForm<z.infer<typeof formSchema>>({
defaultValues: {
email: '',
password: '',
},
resolver: zodResolver(formSchema),
});
// In JSX
<Form {...form}>
<FormField
control={form.control}
name="email"
render={({field}) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</Form>;src/
├── components/ # Reusable UI components
│ ├── ui/ # shadcn/ui base components
│ └── [Component]/ # Component folders with .tsx, .test.tsx, .stories.tsx
├── pages/ # Page components (route-level)
│ ├── account/ # Account-related pages
│ ├── automation/ # Automation feature pages
│ └── platform/ # Platform feature pages
├── shared/ # Shared code across the app
│ ├── components/ # Shared components
│ ├── hooks/ # Custom React hooks
│ ├── stores/ # Zustand stores
│ ├── queries/ # React Query query keys
│ ├── mutations/ # React Query mutations
│ ├── middleware/ # Generated GraphQL types and hooks
│ ├── util/ # Utility functions
│ └── constants.tsx # App-wide constants
├── graphql/ # GraphQL query/mutation files
├── hooks/ # App-level hooks
├── mocks/ # MSW mocks for testing
└── styles/ # Global styles
test/
└── playwright/ # Playwright E2E tests
├── pages/ # Pages split into module-like units
├── tests/ # Tests split into module-like units
├── utils/ # Test helper functions
└── fixtures/ # Test fixtures
@/maps tosrc/- Always use
@/for imports fromsrc/ - Example:
import {Button} from '@/components/ui/button';
- Location: Co-located with components (e.g.,
Button.test.tsxnext toButton.tsx) - Use Testing Library:
@testing-library/react,@testing-library/user-event - Test utilities:
src/shared/util/test-utils.tsx - Setup file:
.vitest/setup.ts - Use MSW for API mocking in tests
- Pattern:
import {render, screen, userEvent} from '@/shared/util/test-utils';
import {expect, it} from 'vitest';
it('should render button', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', {name: /click me/i})).toBeInTheDocument();
});- Location:
test/playwright/ - Fixtures Pattern: Use independent fixtures combined with
mergeTests(following Liferay pattern) - Fixtures Location:
test/playwright/fixtures/ - Available Fixtures:
loginTest()- ProvidesauthenticatedPage(function, call with())projectTest- Providesproject(auto-creates and cleans up)workflowTest- Providesworkflow(requiresprojectwhen merged)
The use Hook: Playwright fixtures use a use callback that:
- Setup (before
use) - Creates resources - Test Execution (during
use) - Your test runs - Cleanup (after
use) - Automatically cleans up resources
// Example fixture structure
export const projectTest = base.extend({
project: async ({page}, use) => {
// SETUP: Create project
const project = await createProject(page);
// GIVE TO TEST: Your test runs here
await use(project);
// CLEANUP: Delete project (runs after test)
await projectsPage.deleteProject(project.id);
},
});Using Fixtures with mergeTests:
import {expect, mergeTests} from '@playwright/test';
import {loginTest, projectTest, workflowTest} from '../../fixtures';
// Combine fixtures at the top of your test file
export const test = mergeTests(loginTest(), projectTest, workflowTest);
test.describe('My Tests', () => {
test('my test', async ({authenticatedPage, project, workflow}) => {
// All fixtures are available:
// - authenticatedPage (from loginTest)
// - project (from projectTest)
// - workflow (from workflowTest)
await authenticatedPage.goto(`/projects/${project.id}/workflows/${workflow.workflowId}`);
// Test logic here
// Fixtures automatically clean up after test
});
});Common Patterns:
// Test needs only authentication
export const test = mergeTests(loginTest());
// Test needs project
export const test = mergeTests(loginTest(), projectTest);
// Test needs everything
export const test = mergeTests(loginTest(), projectTest, workflowTest);Key Principles:
- Fixtures are independent - don't extend each other
- Use
mergeTestsin test files to combine fixtures loginTest()is a function - call it with()- Fixtures automatically clean up resources after tests
- Each fixture provides specific fixtures (see table below)
| Fixture | Provides | Requires |
|---|---|---|
loginTest() |
authenticatedPage |
Nothing |
projectTest |
project |
page (auto-authenticated) |
workflowTest |
workflow |
page + project (when merged) |
- Location:
tailwind-mergepackage - Merges Tailwind classes and resolves conflicts
- Always use this instead of template literals for class names
import {twMerge} from 'tailwind-merge';
<div className={twMerge('base-class', condition && 'conditional-class', className)} />;- Location:
src/shared/constants.tsx - Important constants:
DEVELOPMENT_ENVIRONMENT = 0STAGING_ENVIRONMENT = 1PRODUCTION_ENVIRONMENT = 2AUTHORITIES- User rolesVALUE_PROPERTY_CONTROL_TYPES- Form control types
- Routes defined in
src/routes.tsx - Public routes:
/login,/register,/password-reset, etc. - Private routes require authentication via
PrivateRoutecomponent - Access control via
AccessControlcomponent for specific authorities - Embedded routes:
/embedded/*for embedded workflow builder
- Use React Router's
useNavigate()hook - Use
Linkcomponent for internal navigation - Handle authentication redirects in route loaders
- Use
tmacro for translations:import {t} from '@lingui/macro'; - Extract strings:
npm run lingui:extract - Compile:
npm run lingui:compile - Translation files:
src/locales/[lang]/messages.po
npm run dev- Start dev server (Vite on 127.0.0.1:5173)npm run build- Production buildnpm run test- Run Vitest testsnpm run test:e2e- Run Playwright testsnpm run lint- Run ESLintnpm run format- Format with Prettier + fix ESLintnpm run typecheck- TypeScript type checkingnpm run codegen- Generate GraphQL types and hooks
- Backend API:
localhost:9555(proxied through Vite) - Dev server:
127.0.0.1:5173 - Environment variables:
.env.local(not committed) - Feature flags: Set in
.env.localwithVITE_FF_*prefix
- Use Error Boundaries for component error handling
- Use React Query's error states for data fetching errors
- Show user-friendly error messages
- Use React Query's
isLoadingfor data fetching - Use
PageLoadercomponent for page-level loading - Use skeleton loaders for better UX
- Check flags:
const ff_1234 = useFeatureFlagsStore()('ff-1234'); - Conditionally render based on flags
- Flags defined in backend, accessed via
useApplicationInfoStore
- Check auth:
useAuthenticationStore((state) => state.authenticated) - Login:
useAuthenticationStore((state) => state.login)(email, password, rememberMe) - Public routes don't require auth
- Private routes automatically redirect to
/loginif not authenticated
- ❌ Don't use spaces in curly braces:
{ foo }→ Use{foo} - ❌ Don't create new stores for simple local state → Use
useState - ❌ Don't mutate Zustand state directly → Use
set()function - ❌ Don't use template literals for class names → Use
twMerge() - ❌ Don't import from
src/directly → Use@/alias - ❌ Don't skip
useShallowwhen selecting multiple store values - ❌ Don't write tests without using test utilities from
test-utils.tsx - ❌ Don't forget to set
displayNameon forwardRef components - ❌ Don't use
.lengthin JSX expressions → Extract to variable - ❌ Don't create conditional object keys → Use separate objects
- ❌ Don't use abbreviations for variables/parameters:
e,err,res,req,val,idx→ Use full names:event,error,response,request,value,index - ❌ Don't use single-letter parameters in methods:
(a, b),(acc, item)→ Use descriptive names:(firstNumber, secondNumber),(accumulator, item) - ❌ Don't use em dashes (—) or en dashes (–) → Use regular hyphens (-) instead
- ❌ Don't add comments, only allowed comments are just above a
useEffectexplaining their behaviour in human language - ❌ Don't put evaluations inside function arguments → always extract into a variable
- Config:
codegen.ts - Generates TypeScript types and React Query hooks
- Run
npm run codegenafter updating GraphQL files - Generated files in
src/shared/middleware/(don't edit manually)
- Component stories in
*.stories.tsxfiles - Run:
npm run storybook - Build:
npm run build-storybook - Stories help document component usage and variants
- The app supports both Automation and Embedded modes (feature flag controlled)
- Monaco Editor is used for code editing (YAML, JSON, etc.)
- Workflow editor is built with
@xyflow/reactfor node-based workflow visualization and editing - Rich text inputs use TipTap (ProseMirror-based) for formatted text editing with mentions
- PostHog is used for analytics (conditionally loaded)
- CommandBar is used for help hub (conditionally loaded)
- MSW (Mock Service Worker) is used for API mocking in tests