Thank you for contributing to the OpenFrame OSS Frontend! This guide covers code style conventions, branch naming, the PR process, commit message format, and review checklist.
Before contributing, make sure you have:
- Read the Quick Start Guide
- Set up your Development Environment
- Joined the OpenMSP Slack community for discussions
Note: We do not use GitHub Issues or GitHub Discussions for tracking work. All coordination happens in the OpenMSP Slack community.
All new code must be written in TypeScript with strict typing. Avoid:
anytypes (useunknownwith type guards if necessary)- Type assertions (
as SomeType) unless absolutely required - Non-null assertions (
!) without a comment explaining why it's safe
| Type | Convention | Example |
|---|---|---|
| React components | kebab-case.tsx |
device-details-view.tsx |
| React hooks | use-*.ts or use-*.tsx |
use-device-details.ts |
| Utility functions | kebab-case.ts |
device-action-utils.ts |
| Type definitions | *.types.ts |
device.types.ts |
| GraphQL queries | *-queries.ts or *-relay.ts |
devices-queries.ts |
| Zustand stores | *-store.ts |
mingo-messages-store.ts |
// ✅ Named export for components
export function DeviceDetailsView({ device }: DeviceDetailsViewProps) {
return <div>{device.name}</div>;
}
// ✅ Props interface named after component
interface DeviceDetailsViewProps {
device: Device;
onArchive?: () => void;
}
// ❌ Avoid default exports for components
export default function DeviceDetailsView() { ... }// ✅ Hooks return typed objects (not arrays) for multi-value returns
export function useDeviceDetails(deviceId: string) {
const query = useQuery({
queryKey: ['device', deviceId],
queryFn: () => fetchDevice(deviceId),
});
return {
device: query.data,
isLoading: query.isLoading,
error: query.error,
refetch: query.refetch,
};
}- Use absolute imports with the
@/alias (configured forsrc/) - Group imports: external libraries → internal
@/lib→ internal feature → types
// External
import { useQuery } from '@tanstack/react-query';
import { z } from 'zod';
// Internal infrastructure
import { apiClient } from '@/lib/api-client';
import { runtimeEnv } from '@/lib/runtime-config';
// Feature-local
import { DeviceCard } from './device-card';
import type { Device } from '../types/device.types';ESLint owns the rules, Prettier owns the formatting.
Both rule sets come from the shared config inside @flamingo-stack/openframe-frontend-core
(eslint-config/), not from this repo — see its README.
npm run lint # ESLint, the fast pass
npm run lint:ci # What CI blocks on (the fast pass minus the relay/unused-fields backlog)
npm run lint:fix # ESLint autofix
npm run format:fix # Auto-fix formatting
npm run format # Check without fixingDo not manually configure tab/space counts or line lengths — let Prettier handle it, and note that
// eslint-disable comments do nothing here (noInlineConfig): fix the finding, or add a named,
files:-scoped block to eslint.config.mjs explaining why it cannot be fixed.
Use descriptive branch names following this convention:
<type>/<short-description>
| Type | When to Use | Example |
|---|---|---|
feat/ |
New feature | feat/device-bulk-archive |
fix/ |
Bug fix | fix/token-refresh-race-condition |
chore/ |
Maintenance, deps, tooling | chore/update-relay-to-v21 |
refactor/ |
Code refactoring without behavior change | refactor/extract-api-client |
docs/ |
Documentation updates | docs/add-architecture-diagram |
style/ |
Pure formatting/style changes | style/prettier-format-fix |
git checkout -b feat/script-execution-history-view
git checkout -b fix/devices-table-infinite-scroll-reset
git checkout -b chore/upgrade-next-to-16-3Follow the Conventional Commits specification:
<type>(<scope>): <short summary>
[optional body]
[optional footer]
| Type | Description |
|---|---|
feat |
New feature |
fix |
Bug fix |
chore |
Build process, dependency updates |
docs |
Documentation only |
refactor |
Code change that neither fixes a bug nor adds a feature |
style |
Formatting, whitespace (no logic change) |
test |
Adding or fixing tests |
perf |
Performance improvement |
Use the feature domain as scope when relevant:
feat(devices): add bulk archive action to device table
fix(tickets): prevent duplicate approval request rendering
chore(deps): upgrade @tanstack/react-query to 5.90
- Use the imperative mood: "add" not "added" or "adds"
- Maximum 72 characters in the summary line
- No period at the end of the summary
Run all quality checks locally:
# Type check
npm run type-check
# Lint + formatting
npm run lint
npm run format
# Relay compilation
npm run relay
# Verify build passes
npm run buildAll checks must pass before the PR is submitted.
Include in your PR description:
- What: Summary of changes
- Why: Motivation / problem being solved
- How: Implementation approach (for non-obvious changes)
- Testing: How you verified the changes work
- Keep PRs focused and reasonably sized
- One logical change per PR when possible
- Large features should be broken into smaller, reviewable PRs
- Include only changes relevant to the stated purpose
Before requesting review, verify:
-
npm run type-checkpasses -
npm run lintreports nothing new in the files you touched -
npm run formatpasses -
npm run relaycompiles successfully - No hardcoded credentials, secrets, or tokens
- No
console.logstatements (use proper error handling) - New hooks have explicit return types
- Form inputs validated with Zod
- API calls go through
apiClient(not rawfetch) - No
anytypes introduced - Commit messages follow Conventional Commits format
When reviewing a PR, check:
- Logic correctness and edge cases
- TypeScript types are accurate and not over-widened
- No security issues (XSS, exposed secrets, unvalidated input)
- Consistent with existing patterns in the codebase
- Performance implications — but NOT missing
useMemo/useCallback: the React Compiler (reactCompiler: true) memoizes automatically. What to look for instead is areact-hookslint finding, which means the compiler bailed out of that component - New queries use proper cache key arrays
- Relay fragments follow the established pattern
Follow the existing domain structure exactly:
mkdir -p src/app/\(app\)/my-feature/{components,hooks,queries,types,utils}
touch src/app/\(app\)/my-feature/page.tsx- Add the query string to
src/app/(app)/<domain>/queries/<domain>-queries.ts - Add the hook to
src/app/(app)/<domain>/hooks/use-<entity>.ts - For Relay-based queries, add the fragment to
src/graphql/<domain>/
- Keep stores focused on a single domain
- Export typed selectors to avoid over-subscribing to state changes
- Do not put server state in Zustand (use TanStack Query instead)
For questions about contribution process or design decisions: