Builder UI – AI Operative Guide

TL;DR for Agents

  • Workdir /builder-ui; default port 52002; backend must be running.
  • Run pnpm install first. Bug fixes start with pnpm test:no-coverage; lint with pnpm test:lint.
  • Styling: Use Tailwind for component styles. Edit app/src/app.css only for global design system primitives (buttons, inputs, typography); run pnpm build:css if needed; never touch app/src/app.dist.css.
  • Lingui strings belong inside components/functions only.
  • Never edit generated files: app/src/icons/index.tsx, app/src/app.dist.css, app/src/gql/schema.json.
  • No destructive git resets; never revert user changes.

Audience: AI agents only. Assumes you can run commands, read code, and follow guardrails.

Specs Directory

The specs/ directory is a symlink to ../specs (a separate repository). If the symlink is broken or the specs repo is not available, continue without specs access.

Ignored Paths

  • specs/archive/ - Archived specifications (if available). NEVER read, reference, or use these files.

Prerequisites

Before starting work:

  • Backend must be running (GraphQL dependency)
  • Run pnpm install
  • Working directory: /builder-ui

Task Type → Primary Actions

Task TypePrimary ToolsKey SectionsFirst Commands
Bug fixRead, Edit, BashDebugging Playbook, Architecturepnpm test:no-coverage
New featureTask (Explore), TodoWrite, EditCore Principles, Task RecipesExplore codebase first
Code reviewTask (code-reviewer)Testing Guide, Core PrinciplesReview changes
RefactoringRead, Edit, TaskArchitecture, Core PrinciplesUnderstand structure first
TestingRead, WriteTesting Comprehensive Guidepnpm test:no-coverage
StylingEdit, BashFrontend Conventions (Styling)Tailwind for components; app.css for globals only
i18n/TranslationEdit, BashFrontend Conventions (i18n)pnpm extract

Context & Tech Stack

Tech Stack

  • Framework: React 18.3 + React Router 7
  • Build: Vite 7
  • Styling: Tailwind CSS 3.4 (prefer Tailwind for new code)
  • State: Apollo Client (GraphQL) + Immer
  • Language: JavaScript/JSX with TypeScript support (StandardJS style)
  • Testing: Vitest + Cypress + React Testing Library
  • i18n: Lingui (en, es, fr)

Runtime & Tooling

  • Node: >=22
  • Package Manager: pnpm 10.x
  • Primary Path Alias: @app/src
  • Testing Alias: @testing/*app/src/@testing/*
  • Filename Convention: kebab-case (enforced by ESLint)

Entry Points & Key Files

  • Entry: app/src/index.jsx mounts router from app/src/routes.jsx
  • Root Layout: app/src/root-layout.jsx (Apollo provider, theme, alerts, feature-flag modal, SystemError boundary, keyboard shortcuts)
  • App Config: app/src/config.jsx
  • Global Styles: app/src/app.css (source) → app/src/app.dist.css (compiled, never edit directly)
  • Routing: app/src/routes.jsx - React Router 7 route tree
  • GraphQL Setup: app/src/gql/index.jsx (Apollo Client with custom type policies)
  • GraphQL Schema: app/src/gql/schema.json (generated)
  • i18n Setup: app/src/i18n.jsx

Architecture & Structure

Data Layer

  • GraphQL: Apollo Client @ /app/api/v0/graphql
  • Auth: Bearer token from authToken cookie
  • WebSocket: Subscriptions via WSS
  • Cache: Custom type policies in app/src/gql/index.jsx (the typePolicies export, ~lines 27-172)
  • REST (Secondary): /api/v1/* for Identity Service (user management)

Backend Architecture Flow

Frontend (React)
↓ GraphQL Query/Mutation
Platform (Elixir/Phoenix, `/app/api/v0/graphql`)
├─→ MongoDB (tenant/customer data, basic integrations)
├─→ Postgres (Oban jobs, Bridge/Lasso integrations, internal data)
├─→ Identity Service (Node.js/Express, `/api/v1/users/*`) ⇄ MongoDB (user auth data)
├─→ Forms API (Node.js/Express) - form processing/validation
├─→ Workflows API (Node.js/Express) - workflow execution
└─→ PDF API - PDF generation
Identity Service can call Platform back for user extended attributes (bi-directional)

Directory Structure

Core Application Pages

app/src/pages-builder/ - Builder/configuration interface (form designer, workflow, dashboard, permissions, publishing)

app/src/pages-runner/ - End-user runtime (run, edit, view forms, workflow actions, document history)

app/src/pages/ - System administration (home, identity management, integrations, audit, spaces, settings, usage, permissions)

app/src/pages-anonymous/ - Public form submission (unauthenticated, separate Apollo client)

Core Engines

app/src/formbot/ - Form rendering engine

  • index.jsx - Main formbot instance, gadget registration, validation
  • gadgets/ - Many gadget types including Text, Dropdown, Repeater, Table, DataLookup, and more
  • engine/ - Core rendering and state management
  • decorators/ - Validation, progressive disclosure, runtime enhancements
  • Architecture: Plugin-based; each gadget has manifest, config component, runtime component, validation
  • Data-driven forms from templates

app/src/flowbot/ - Visual workflow designer and execution engine

  • Custom-built linear workflow editor with drag-and-drop (using voronoi-dnd)
  • engine/ - Visual editor, viewer, configuration panel, validation, simulation
  • steps/ - 9 step types: approval, task, notification, formfill, acknowledge, conditional, integration, echo, trigger
  • components/ - UI components (email builder, person picker, etc.)
  • Supports nested subflows (denial paths, conditional branches)
  • Integrates with formbot for form data flow

app/src/voronoi-dnd/ - Generic drag-and-drop library

  • Uses Voronoi diagrams for intelligent drop target detection
  • Framework-agnostic core in voronoi.jsx
  • draggable.jsx, drop-zone.jsx, gatherer.jsx, item.jsx, context.jsx

app/src/voronoi-dnd-formbot/ - Form-specific DND implementation

  • Adapter between voronoi-dnd and formbot
  • Handles nested containers (sections, repeaters, tables)
  • Grid snapping, empty states, gadget palette dragging

Shared Infrastructure

app/src/components/ - Shared UI components: layouts, modals (modal-centered.jsx, modal-page.jsx), data tables, identity pickers, feature flags, error boundaries, spinners

app/src/ui/ - Complex system primitives and utilities: alerts, popovers, tabs, tooltips, lookup, theme utilities, a11y helpers, and shadcn components (dropdown-menu, sheet, sidebar, skeleton)

app/src/icons/ - Auto-generated SVG icons (never edit index.tsx manually, use import scripts)

app/src/illustrations/ - Decorative SVGs for empty states and success screens

app/src/gql/ - GraphQL config: Apollo Client setup (index.jsx), schema (schema.json), type policies

app/src/@testing/ - Test utilities: Mockley, test helpers, fixtures. Setup in .test/setup.js

Key Architectural Patterns

Separation of Concerns

  • Configuration/Admin: pages-builder/
  • End-user Runtime: pages-runner/
  • System Administration: pages/
  • Shared UI: components/
  • Design Primitives: ui/

Form System Architecture

  • Core Engine: formbot/ with many gadget types
  • Generic DND: voronoi-dnd/
  • Form-specific DND: voronoi-dnd-formbot/
  • Data-driven: Forms rendered from templates

Workflow System

  • Visual Designer & Execution: flowbot/ - custom-built linear workflow editor with drag-and-drop
  • Integration: Workflows integrate with formbot for form data flow

State Management

  • Server State: Apollo Client (GraphQL)
  • Local UI State: React state
  • Immutable Updates: Immer
  • Cross-cutting Concerns: Context API

Data Sharing Patterns

  • Preferred: Use dedicated Context API for specific data or Apollo cache
  • Legacy (avoid in new code): useOutletContext exists from React Router 7 migration; prefer Context/Apollo for new features

Configuration & Infrastructure

Environment Variables

Critical environment variables used throughout the application:

  • VITE_SENTRY_RELEASE: Sentry release version (enables Sentry if set)
  • VITE_SENTRY_DSN: Sentry DSN for error reporting
  • VITE_SENTRY_ENV: Environment name (development/staging/production)
  • PUBLIC_URL: Base path for static assets (used throughout for icon paths)
  • PORT: Dev server port (default: 52002 from build.service.json5)
  • NO_HMR: Disable hot module replacement if set

Configuration lives in app/src/config.jsx:

  • Sentry setup with custom beforeBreadcrumb/beforeSend
  • Apollo Client config (name, version, URI)
  • AnnounceKit widget config
  • window.loggedInUser used throughout for user context

Apollo Cache Type Policies

Location: app/src/gql/index.jsx (the typePolicies export, ~lines 27-172)

Why they exist: Workarounds for caching issues caused by API design/query patterns. Ideally would be addressed at API level.

Existing policies (understand these when reading code, avoid adding new ones if possible):

  • groupsConnection, membersConnection - Merge paginated edges, dedupe by __ref
  • Dataset - Custom key includes formVersion.id to cache different versions separately
  • Field - Disable normalization (keyFields: false) - too dynamic to cache
  • Document.viewer - Always replace (don't merge) to avoid stale data
  • ActionsPaginatedConnection - Merge edges with custom sort

If experiencing cache issues: First investigate if query/API can be fixed. Only add type policy as last resort.

Error Handling & Monitoring

Error Boundary:

  • Root layout uses SystemError component (app/src/components/system-error.jsx)
  • Catches React errors, reports to Sentry with context
  • 401 errors redirect to /auth, permission errors redirect to Forbidden component

Sentry Integration:

  • Configured in app/src/config.jsx with custom breadcrumb/error context
  • Only enabled when VITE_SENTRY_RELEASE is set

GraphQL Errors:

  • Network 401 → redirects to /auth?return_to=<current>
  • Permission errors → shows Forbidden component
  • Use ExplicitError or GraphQLError components for manual error display

Content Security Policy (CSP)

Adding new external domains:

  1. Edit scripts/generate-csp/index.js
  2. Add domain to appropriate policy object (script-src, connect-src, img-src)
  3. Run node scripts/generate-csp to verify

CSP includes third-party integrations (AnnounceKit, ChurnZero, Sentry), allows 'unsafe-inline', WebSocket across Kuali domains, S3 image buckets.

HTML Sanitization

Use app/src/components/sanitize.jsx for sanitizing user-generated HTML in rich text gadgets and email content.

Git Hooks (Husky)

Pre-commit runs Prettier + ESLint on staged files (configure via git config hooks.validatebuilderui on|off|custom). Bypass with --no-verify.

ESLint Configuration

Config: eslint.config.mjs (ESLint 9 flat config)

  • Enforces: kebab-case filenames, GraphQL schema validation, StandardJS style
  • Max warnings: 112 allowed
  • Many a11y rules disabled - still follow best practices (aria-labels, role attributes, aria-describedby, aria-invalid)

Core Development Principles

Prioritize thoroughness over speed. Correctness and consistency matter more than quick completion. Always read existing code, understand patterns, write tests, and verify changes work properly. Taking time to do it right prevents technical debt.

1. Match Existing Patterns

Always read code before modifying it. Never propose changes to files you haven't read. Before adding or modifying code:

  • Read the target file and related files
  • Identify existing patterns (naming, structure, error handling, imports)
  • Match the established conventions exactly
  • Look for similar functionality nearby and follow that approach
  • Use the same libraries/utilities already in use (don't introduce new ones for existing problems)

Example: If modifying a GraphQL mutation file and all existing mutations use mapValues(keyBy(...)) for error handling, use that same pattern - don't introduce a new approach.

2. Know Your JavaScript/ES6 Limitations

Check MDN documentation - arrays have .includes(), not .contains()

3. Data Structure Consistency Over Cleverness

Use clear, semantic field names (type, parts). Don't overload fields for multiple purposes.

4. Recursive Pattern Recognition

When handling nested structures, check for nested patterns first before handling leaf nodes.

5. Test Data Must Mirror Production

Test fixtures must use exact same structure as production code with all required fields.

6. Filter Early, Process Clean Data

Remove incomplete/invalid data before processing - validate at boundaries.

7. Handle Both Legacy and New Formats

Support both old and new patterns during transitions. Transform at edges, not throughout.

8. Defensive Programming

Always validate data existence. Check arrays/objects exist before operations. Return early with sensible defaults.

9. Favor Clarity Over Cleverness

Avoid premature abstraction. Compose small components. Separate business logic from presentation.

10. Avoid Over-Engineering

Only make requested changes. Keep solutions simple. Don't add features beyond requirements. Three similar lines > premature abstraction.

11. Test Behavior, Not Implementation

Test what code does, not how. Focus on user-visible behavior. Cover happy path + edge cases. Avoid testing internals.

12. Avoid useEffect - Use Only When Necessary

Only use useEffect for external system synchronization (WebSockets, DOM manipulation, third-party libraries, analytics).

Never use useEffect for:

  • Deriving state → compute during render
  • Event handling → use onClick/onChange handlers
  • Data fetching → use Apollo queries or React Router loaders
  • Initialization → use useMemo or useState initializer

Always include cleanup functions to prevent memory leaks.

13. Minimal Diffs

Only change what's necessary for your task. Don't rename variables, reformat code, fix typos, add/remove whitespace, or refactor unrelated logic. Keep diffs focused on the actual functional change.

However, when your change makes something obsolete:

  • DO remove variables that become unused
  • DO simplify conditions that become trivial
  • DO remove unnecessary operations (like .filter(Boolean) on non-conditional arrays)

These are not "extra" changes—they're completing the work. A variable that's assigned-but-never-used after your change should be removed as part of the same change. Every line touched increases review burden and merge conflict risk, but leaving dead code is worse.

14. Eliminate Intermediate Variables

When a variable is simply assigned once and never reassigned, use the original value directly instead of creating an intermediate variable.

Bad:

const gadgets = anonymousDisabledGadgets
if (gadgets.has(gadget.key)) return true

Good:

if (anonymousDisabledGadgets.has(gadget.key)) return true

Exception: Keep intermediate variables when they:

  • Improve readability for complex expressions
  • Are computed values (not simple reassignments)
  • Are used multiple times in a way that would duplicate complex logic

Self-Review Checklist

Before considering work complete, review your changes for:

  1. Unnecessary complexity: Can any intermediate variables be eliminated?
  2. Dead code: Are there variables declared but never used after your changes?
  3. Redundant operations: Did you remove .filter(Boolean) when arrays no longer have conditional elements?
  4. Simplified conditions: Did you fully simplify boolean logic after removing flags?
  5. Consistent patterns: Does your code match the simplicity level of surrounding code?

This self-review catches issues that PR reviewers would otherwise need to point out.


Process & Resource Safeguards

Test Execution Limits

  • Never run tests in an infinite retry loop. If tests fail twice in a row with the same error, stop and report the issue to the user rather than retrying.
  • Limit concurrent test processes. Never spawn more than one test runner process at a time. Wait for the previous test run to complete before starting another.
  • Kill orphaned processes. If a test run times out or fails, ensure the process is terminated before starting a new one.

Failure Handling

  • After 2 consecutive identical failures, stop and ask the user for guidance rather than continuing to retry.
  • If a command appears to hang (no output for 2+ minutes), terminate it and report the issue rather than spawning additional processes.
  • Never run watch mode tests (pnpm test:watch) in automated fix loops - use pnpm test:no-coverage for single runs instead.

Memory-Intensive Operations

  • Avoid parallel test runs. Run pnpm test:no-coverage sequentially, not in parallel with other resource-intensive commands.
  • Check for running processes before spawning new test runs. If a test process is already running, wait for it to complete.

Frontend Conventions

Authentication & Data Flow

  • Backend topology: See "Backend Architecture Flow" diagram above
  • Auth flow: 401 redirects to /auth; uses window.loggedInUser or fetches from /api/v1/users/current; token in authToken cookie
  • SSO users: Have ssoId field (SAML/CAS/LDAP). Cannot set/change passwords (HTTP 400)
  • Password users: Have passwordDigest (bcrypt, 10 rounds). Backend never returns password; UI enforces complexity rules
  • Institutions can mix both auth types; always check user.ssoId per-user

Use CenterModal (app/src/components/modal-centered.jsx):

  • Automatically provides role="dialog", aria-modal="true", focus trap, ESC/backdrop handling, animations
  • Required: Modal heading must use id="modal-title" (hardcoded expectation)
  • Focus management: You're responsible for returning focus to the trigger element
    const triggerRef = useRef()
    // After modal dismiss:
    setTimeout(() => triggerRef.current?.focus(), 100)
  • Animations: Uses Preserve + Transition components (300ms default)

For full-page/side-drawer modals, use modal-page.jsx with nesting support.

Alerts & Toasts

Use useAlerts() from app/src/ui/alerts.jsx:

  • type1: Dialog
  • type2: Banner
  • type3: Toast (auto-dismiss after 4 seconds)
const alerts = useAlerts()
alerts.type3(t`Password updated successfully`, 'success')

State Management

Immutable Updates: Always spread entire objects in onChange handlers (never partial updates). Use Immer for complex nested updates.

Styling Conventions

  • Local component styles: Use Tailwind CSS utilities (preferred for all component-specific styling)
  • Global styles: Edit app/src/app.css only for design system primitives (buttons, inputs, typography, etc.); never edit app/src/app.dist.css (generated)
  • Error text: text-red-500
  • Disabled inputs: cursor-not-allowed bg-light-gray-100
  • Theme colors: CSS vars like --bg, --text-default
  • Tailwind config: tailwind.config.js
  • Animations: tw-animate-css

Accessibility (a11y)

  • Interactive elements: Every control needs ARIA labeling
  • aria-describedby: Reference all descriptive content (help text, format hints, error messages) using space-separated IDs
  • Validation messages: Require role="alert" and must be referenced via aria-describedby
  • Invalid inputs: Set aria-invalid="true" when errors exist
  • Modal headings: Must use id="modal-title" for proper announcement

Internationalization (i18n)

Use Lingui macros (@lingui/react/macro):

import { Trans } from '@lingui/react/macro'
import { useLingui } from '@lingui/react'
// In component
const { t, i18n } = useLingui()
<Trans>Welcome message</Trans>
const message = t`Dynamic message`
// Date/number formatting
i18n.date(new Date())
i18n.number(1234.56)

Rules:

  • Never call t() at module root - only inside components/functions
  • Supported locales: en, es, fr
  • Messages live in app/src/locales/<locale>/messages.{po/js}
  • After adding/changing strings: pnpm extract to update .po catalogs
  • Commit .po changes
  • build:i18n compiles catalogs (dev scripts watch and recompile)

GraphQL Patterns

Mutation Responses (Union Types)

Expect union responses and check __typename:

const result = await updateUser({ variables: { id, input } })
if (result.data.updateUser.__typename === 'InvalidFieldErrors') {
// Error structure: { errors: [{ field, reason }] }
const errorMap = mapValues(
keyBy(result.data.updateUser.errors, 'field'),
'reason'
)
setErrors(errorMap)
} else {
// Success: result.data.updateUser is the User type
handleSuccess(result.data.updateUser)
}

Data Sharing Pattern

For new code: Use dedicated Context API or Apollo cache for sharing data between components.

Existing pattern (legacy from React Router 7 migration - avoid in new code):

// Some existing routes use useOutletContext - when reading existing code:
const { initialUser, onChange } = useOutletContext()

Schema Updates

When backend schema changes: node scripts/generate-graphql-schema (requires backend), restart editor/ESLint. ESLint uses schema to validate queries - regenerate if unknown field errors.


Testing

Testing standards and patterns are documented in .claude/rules/testing.md. This file is automatically loaded when working with test files (*.test.* or *.spec.*).


Task-Based Recipes

Add/Modify Routes

  1. Open app/src/routes.jsx (React Router 7 createBrowserRouter)
  2. Import the page component
  3. Extend the route tree:
    • Use Navigate helper to preserve search params
    • Wrap auth-required branches with Protected
    • Use layouts like AppLayout for standard nav/header
  4. For nested/modal routes, nest under parent route
{
path: '/users/:id',
element: <Protected><AppLayout><UserPage /></AppLayout></Protected>,
children: [
{ path: 'edit', element: <UserEditModal /> }
]
}

Add/Modify Pages

  1. Create component under appropriate app/src/pages* directory:
    • pages-builder/ for configuration UI
    • pages-runner/ for end-user runtime
    • pages/ for system administration
    • pages-anonymous/ for public/unauthenticated
  2. Wrap in AppLayout if it needs standard nav/header
  3. Colocate page-specific GraphQL operations near the page
  4. Wire route in app/src/routes.jsx
  5. Add Protected wrapper if authentication required
  6. Add feature-flag gating if needed

Update/Add GraphQL Operations

  1. Colocate query/mutation/fragments with the page/component
  2. If ESLint complains about unknown fields:
    • Regenerate schema: node scripts/generate-graphql-schema (requires backend running)
    • Restart editor/ESLint
  3. Consider cache typePolicies only if custom merge/key needed (in app/src/gql/index.jsx)
  4. Run pnpm test:no-coverage and pnpm test:lint

Styling Changes

  1. Component styles: Use Tailwind CSS utilities for all component-specific styling
  2. Global styles: Edit app/src/app.css only for design system primitives (buttons, inputs, typography, etc.)
  3. Never edit app/src/app.dist.css (generated)
  4. For global CSS changes:
    • Run pnpm build:css manually, or
    • Dev watcher handles it automatically
  5. Verify changes with pnpm test:no-coverage

Feature Flags

Check Existing Flags

  • Location: app/src/components/feature-flags.tsx
  • Use exported booleans/helpers in components
  • Modal available via ?center-modal=feature-flags

Add/Modify Flag

  1. Define in app/src/components/feature-flags.tsx (id, default, description)
  2. Gate UI in components/pages
  3. In tests:
    • Use setFlag/clearFlag from @/components/feature-flags to toggle flag state
    • Update .test/setup.js defaults if new flag should be on by default
  4. Cover both flag states (on/off) with tests
  5. Run pnpm test:no-coverage and pnpm test:lint

Removing Feature Flags

When removing permanently enabled feature flags:

  1. Logic Analysis: For each conditional check, determine what the condition evaluates to when the flag is TRUE

    • if (flag) → condition is TRUE, keep the if-block content
    • if (!flag) → condition is FALSE, remove entire if-block
    • if (flag || other) → condition is FALSE, remove entire if-block
    • if (!flag && other) → condition is FALSE, remove entire if-block
    • if (flag && other) → simplifies to if (other)
    • if (!flag || other) → simplifies to if (other)
  2. Variable Cleanup: After removing conditionals, look for variables that are no longer needed:

    • Variables only used in removed conditions
    • Variables that are simple reassignments of other variables
    • Arrays with .filter(Boolean) when all elements are now unconditional
  3. Spread Operator Arrays: When removing conditional spreads:

    // Before:
    const arr = [A, ...(flag ? [B] : []), C].filter(Boolean)
    // After (flag=TRUE):
    const arr = [A, B, C] // Remove filter(Boolean) too
  4. Self-Review: Apply the Self-Review Checklist to catch simplification opportunities

Translations (i18n)

Add/Update UI Copy

  1. Wrap user-facing text in Lingui macros:

    import { useLingui } from '@lingui/react'
    import { Trans } from '@lingui/react/macro'
    ;<Trans>New text</Trans>
    const { t } = useLingui()
    const msg = t`Dynamic message`
  2. Use i18n.date(date) and i18n.number(num) for formatting

  3. Run pnpm extract to refresh .po catalogs

  4. Commit the .po file changes

  5. Run pnpm build:i18n (or let dev watcher handle it)

  6. Run pnpm test:no-coverage

Writing Tests

  1. Add Vitest/Testing Library specs near components or under app/src/__tests__
  2. Import helpers from @testing/*
  3. Use mountApp, renderPage, or renderGadgetConfig as appropriate
  4. Create custom Mockley instance if default mocks insufficient
  5. Load locales: await loadLocale('en') before asserting translated text
  6. Follow Testing Library query priority (prefer getByRole)
  7. Run pnpm test:no-coverage for quick feedback

Regenerate Generated Files

  • Icons: Use scripts/import-icons/ (never edit app/src/icons/index.tsx)
  • GraphQL Schema: node scripts/generate-graphql-schema (requires backend), restart editor after
  • CSP: scripts/generate-csp (when adding external domains)
  • Browser Support: scripts/generate-browser-check

Add/Modify Gadgets

Full guide: app/src/formbot/gadgets/gadgets.mdx (requirements checklist, examples, three-repo structure)

Gadgets live in app/src/formbot/gadgets/. File structure:

app/src/formbot/gadgets/my-gadget/
manifest.jsx # Ties everything together
edit.jsx # Runtime edit (functional component + hooks)
view.jsx # Display component
config.jsx # Configuration UI
validation.jsx # validateShape (custom validator)
utils.js # Business logic (formatting, parsing) - pure functions
icon.svg.jsx # SVG icon
progressive-disclosure.jsx # (optional) Conditional visibility
filters.jsx # (optional) Document list filtering

Modern patterns (functional components, Tailwind, utils separation):

  • Use useState/useRef/useEffect, not class components
  • Use Tailwind CSS for styling
  • Abstract business logic into utils.js (pure functions) - see Currency, PhoneNumber, Table gadgets
  • Include ARIA attributes: aria-labelledby, aria-describedby, aria-required, aria-invalid
  • Reference examples: Text (basic structure), PhoneNumber (modern best practices)

Third-party libraries: Only MIT/BSD/Apache 2.0 licenses - no GPL/AGPL/copyleft. Check package.json license field before adding dependencies.

Testing: Aim for 15-20+ tests (component, utils, edge cases, accessibility). Use renderGadgetConfig/renderGadgetEdit from @testing/setup.

Register in app/src/formbot/index.jsx:

import MyGadget from './gadgets/my-gadget/manifest'
formbot.registerGadget('MyGadget', MyGadget)

Multi-repo: UI in builder-ui/app/src/formbot/gadgets, server validation in platform/lib/formbot/gadgets (Elixir), form processing in forms-api/server/lib/gadgets (Node.js)

Field reference detection: If your gadget/feature stores references to meta.createdBy or meta.submittedBy fields (including extended attributes), update app/src/pages-builder/form/extended-attribute-usage-utils.ts to detect those references. This prevents admins from removing fields that are in use. See existing patterns in that file.


Debugging Playbook

GraphQL

Check DevTools Network → inspect __typename for union types. Verify query includes required fields. Regenerate schema if lint errors (node scripts/generate-graphql-schema). Check Apollo cache. Common: missing __typename checks, stale schema, cache merge issues (see type policies section).

SSO vs Password

Check user.ssoId: present = SSO-only, absent = password auth. Hide password fields when user.ssoId exists. Backend returns HTTP 400 if SSO user attempts password change.

Form State, Modals, Validation

  • Form state: Fields disappearing → spread entire object in onChange (see State Management)
  • Modals: Missing id="modal-title" on heading or focus not returned to trigger (see Modal Usage)
  • Validation: Use touched flags; error containers need role="alert"; reference via aria-describedby; set aria-invalid="true" on inputs with errors

Test Failures

  • Translations: Run pnpm build:i18n, add await loadLocale('en') before assertions
  • GraphQL data: Check app/src/@testing/mocks/, use api.debug() to dump mock data
  • Timezone: All tests run under TZ=Etc/UTC
  • Coverage: Use pnpm test:no-coverage for targeted runs (full suite enforces thresholds)
  • Element not found: Use find* queries (async), follow Testing Library query priority
  • Act warnings: Async state not awaited - wrap in waitFor or use find* queries

Dev Server Issues

Verify PORT (default 52002), check backend running, try pnpm dev:docked, check port conflicts (lsof -i :52002), clear cache (rm -rf node_modules/.vite).

Build Issues

  • CSS: Edit app.css not app.dist.css, run pnpm build:css
  • Translations: pnpm extractpnpm build:i18n
  • GraphQL lint: Regenerate schema
  • Build fails: Run pnpm build (CSS → i18n → JS)

Code Quality & Workflows

Linting & Formatting

  • ESLint: pnpm test:lint
    • Includes GraphQL lint (uses schema to validate queries)
    • Enforces kebab-case filenames
    • StandardJS code style
  • Prettier: pnpm test:prettier
    • Custom import ordering
    • No semicolons, 2-space indent
  • Format: pnpm format (applies Prettier)
  • License check: pnpm test:license

Git & Pull Requests

  • Main branch: master
  • Husky runs pre-commit hooks (Prettier + ESLint)
  • StandardJS: No semicolons, 2-space indent
  • Common commands referenced throughout doc; run pnpm run to see all available scripts

PR Labels (Required)

Every PR must have a "Change type" label:

  • change_type/application - Use for most PRs (code changes, features, bug fixes, tests)
  • change_type/infrastructure - Use only for configuration file changes (CI/CD, build config, etc.)

Add labels when creating PRs:

gh pr create --title "Title" --body "Description" --label "change_type/application"

Pre-Submission Verification

Always run: pnpm test:no-coverage, pnpm test:lint, pnpm test:prettier

Check ESLint on changed files as you work: After modifying or creating files, run pnpm eslint "path/to/changed/file.jsx" before moving on.

Task-specific requirements covered in Task Recipes sections above (GraphQL schema regen, i18n extract, CSS build, etc.)

Common Pitfalls

Don't: edit generated files, call t() at module root, create unnecessary files, add unrequested features, partially update state, test implementation details, assume methods exist


Last Updated: 2025-11-28 Maintained By: AI Agents (update as project evolves)