Skip to content

Context & Props

Extension components receive a context object containing relevant data about the current view and entity being displayed. The context structure varies by extension point.

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;
};
}

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>
);
}

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;
}

For dashboard.* extension points:

interface DashboardContext extends BaseContext {
// Dashboard-specific fields
dateRange?: {
from: Date;
to: Date;
};
// Filters
filters?: {
businessType?: string[];
status?: string[];
};
}
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
}
interface ActionComponentProps<T = any> {
context: T; // Context object
config: any; // Plugin configuration
onComplete?: () => void; // Callback when action completes
}
interface WidgetComponentProps {
context: BaseContext; // Base context
config: any; // Plugin configuration
gridSize?: { // Dashboard grid size
cols: number;
rows: number;
};
}
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>
);
}
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>
);
}

The config prop contains plugin-specific configuration from user settings:

// In your settings component
function 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 component
function MyWidget({ context, config }: WidgetComponentProps) {
const refreshInterval = config.refreshInterval ?? 30;
useEffect(() => {
const interval = setInterval(() => {
refetch();
}, refreshInterval * 1000);
return () => clearInterval(interval);
}, [refreshInterval]);
// ...
}

For better type safety, create typed props interfaces:

types.ts
import type { TabComponentProps } from '@aether/plugins-core';
export interface WorkUnitTabProps extends TabComponentProps<WorkUnitContext> {}
export interface CustomerTabProps extends TabComponentProps<CustomerContext> {}
// MyTab.tsx
import type { WorkUnitTabProps } from './types';
export function MyTab({ context, config }: WorkUnitTabProps) {
// context is fully typed as WorkUnitContext
const { workUnit, workUnitId } = context;
// ...
}

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>
);
}
  1. Destructure Early: Extract needed fields at the top of your component
  2. Type Your Props: Use typed interfaces for better autocomplete
  3. Handle Missing Data: Always provide fallbacks for optional fields
  4. Use Context IDs: Prefer IDs over full objects for queries
  5. Respect Configuration: Honor user settings from config prop