Context & Props
Overview
Section titled “Overview”Extension components receive a context object containing relevant data about the current view and entity being displayed. The context structure varies by extension point.
Common Context Fields
Section titled “Common Context Fields”All contexts include these base fields:
interface BaseContext { organizationId: string; organization?: { id: string; name: string; businessType: string; premiumTier: string; premiumFeatures: string[]; }; userId: string; user?: { id: string; name: string; email: string; };}Work Unit Context
Section titled “Work Unit Context”For work-unit.* extension points:
interface WorkUnitContext extends BaseContext { workUnit: any; // Full work unit object workUnitId: string; // Work unit ID businessType: string; // SERVICE | PROJECT | PRODUCT status: string; // DRAFT | ACTIVE | COMPLETED | etc entityType: string; // 'work-unit' entityId: string; // Same as workUnitId
// Counts timeEntryCount?: number; taskCount?: number; attachmentCount?: number; commentCount?: number;
// Relations customer?: { id: string; name: string; }; assignedUsers?: Array<{ id: string; name: string; }>;}Example Usage:
interface MyTabProps { context: WorkUnitContext; config: any; isCondensed?: boolean;}
export function MyTab({ context }: MyTabProps) { const { workUnit, workUnitId, organizationId } = context;
const { data } = trpc.myPlugin.getData.useQuery({ workUnitId, organizationId, });
return ( <Card> <Text>Work Unit: {workUnit.name}</Text> <Text>Status: {context.status}</Text> <Text>Type: {context.businessType}</Text> </Card> );}Customer Context
Section titled “Customer Context”For customer.* extension points:
interface CustomerContext extends BaseContext { customer: any; // Full customer object customerId: string; // Customer ID entityType: string; // 'customer' entityId: string; // Same as customerId
// Counts workUnitCount?: number; invoiceCount?: number; contactCount?: number;
// Financials totalRevenue?: number; outstandingBalance?: number;}Dashboard Context
Section titled “Dashboard Context”For dashboard.* extension points:
interface DashboardContext extends BaseContext { // Dashboard-specific fields dateRange?: { from: Date; to: Date; };
// Filters filters?: { businessType?: string[]; status?: string[]; };}Component Props Interface
Section titled “Component Props Interface”TAB Extension Props
Section titled “TAB Extension Props”interface TabComponentProps<T = any> { context: T; // Context object (typed by extension point) config: any; // Plugin configuration from settings isCondensed?: boolean; // Whether tab is in condensed mode}ACTION Extension Props
Section titled “ACTION Extension Props”interface ActionComponentProps<T = any> { context: T; // Context object config: any; // Plugin configuration onComplete?: () => void; // Callback when action completes}WIDGET Extension Props
Section titled “WIDGET Extension Props”interface WidgetComponentProps { context: BaseContext; // Base context config: any; // Plugin configuration gridSize?: { // Dashboard grid size cols: number; rows: number; };}Accessing Context Data
Section titled “Accessing Context Data”Work Unit Example
Section titled “Work Unit Example”export function TimerTab({ context, config }: TabComponentProps<WorkUnitContext>) { // Destructure what you need const { workUnit, workUnitId, organizationId } = context;
// Access organization data const isPremium = context.organization?.premiumTier === 'PREMIUM';
// Access counts const existingEntries = context.timeEntryCount || 0;
// Use in queries const { data: timeEntries } = trpc.timer.getTimeEntries.useQuery({ workUnitId, organizationId, });
return ( <Stack> <Text>Tracking time for: {workUnit.name}</Text> <Text>Existing entries: {existingEntries}</Text> {timeEntries?.map(entry => ( <TimeEntryCard key={entry.id} entry={entry} /> ))} </Stack> );}Customer Example
Section titled “Customer Example”export function CustomerInsightsTab({ context }: TabComponentProps<CustomerContext>) { const { customer, customerId, organizationId } = context;
const { data: insights } = trpc.insights.getCustomerInsights.useQuery({ customerId, organizationId, });
return ( <Stack> <Title>{customer.name} - Insights</Title> <Text>Total Revenue: ${context.totalRevenue}</Text> <Text>Work Units: {context.workUnitCount}</Text> </Stack> );}Plugin Configuration
Section titled “Plugin Configuration”The config prop contains plugin-specific configuration from user settings:
// In your settings componentfunction MyPluginSettings({ config, onChange }) { return ( <Stack> <Switch label="Enable notifications" checked={config.notifications ?? true} onChange={(e) => onChange({ ...config, notifications: e.target.checked })} />
<NumberInput label="Refresh interval (seconds)" value={config.refreshInterval ?? 30} onChange={(value) => onChange({ ...config, refreshInterval: value })} /> </Stack> );}
// In your extension componentfunction MyWidget({ context, config }: WidgetComponentProps) { const refreshInterval = config.refreshInterval ?? 30;
useEffect(() => { const interval = setInterval(() => { refetch(); }, refreshInterval * 1000);
return () => clearInterval(interval); }, [refreshInterval]);
// ...}Type Safety
Section titled “Type Safety”For better type safety, create typed props interfaces:
import type { TabComponentProps } from '@aether/plugins-core';
export interface WorkUnitTabProps extends TabComponentProps<WorkUnitContext> {}export interface CustomerTabProps extends TabComponentProps<CustomerContext> {}
// MyTab.tsximport type { WorkUnitTabProps } from './types';
export function MyTab({ context, config }: WorkUnitTabProps) { // context is fully typed as WorkUnitContext const { workUnit, workUnitId } = context; // ...}Condensed Mode
Section titled “Condensed Mode”Some tabs support condensed mode for smaller viewports:
export function MyTab({ context, isCondensed }: TabComponentProps) { return ( <Stack gap={isCondensed ? 'xs' : 'md'}> <Title order={isCondensed ? 4 : 3}> {context.workUnit.name} </Title> {!isCondensed && ( <Text c="dimmed">Additional details...</Text> )} </Stack> );}Best Practices
Section titled “Best Practices”- Destructure Early: Extract needed fields at the top of your component
- Type Your Props: Use typed interfaces for better autocomplete
- Handle Missing Data: Always provide fallbacks for optional fields
- Use Context IDs: Prefer IDs over full objects for queries
- Respect Configuration: Honor user settings from
configprop