@hailer/mcp 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/post-scaffold-hook.cjs +125 -0
- package/.claude/hooks/prompt-skill-loader.cjs +106 -5
- package/.claude/hooks/publish-template-guard.cjs +123 -0
- package/.claude/hooks/skill-loader.cjs +1 -1
- package/.claude/settings.json +22 -0
- package/.claude/skills/MCP-build-data-app-skill/SKILL.md +372 -0
- package/.claude/skills/MCP-publish-template-skill/SKILL.md +278 -0
- package/.claude/skills/MCP-scaffold-hailer-app-skill/SKILL.md +329 -49
- package/.claude/skills/building-hailer-apps-skill/SKILL.md +274 -9
- package/.claude/skills/hailer-app-builder/SKILL.md +340 -0
- package/.claude/skills/spawn-app-builder/SKILL.md +366 -0
- package/CHANGELOG.md +24 -0
- package/dist/app.js +8 -0
- package/dist/mcp/tools/app.d.ts +7 -0
- package/dist/mcp/tools/app.js +997 -1
- package/package.json +1 -1
|
@@ -7,6 +7,35 @@ description: Complete guide for building Hailer apps with the @hailer/app-sdk -
|
|
|
7
7
|
|
|
8
8
|
Complete guide for building Hailer apps using the `@hailer/app-sdk` package. This skill covers critical patterns and gotchas that prevent data loading issues.
|
|
9
9
|
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## ⚠️ CRITICAL FIRST STEP: Fix main.tsx with ChakraProvider
|
|
13
|
+
|
|
14
|
+
**THE SCAFFOLD TEMPLATE IS MISSING ChakraProvider!** You MUST fix this FIRST or styles won't render:
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// src/main.tsx - REPLACE the entire file with this:
|
|
18
|
+
import React from 'react'
|
|
19
|
+
import ReactDOM from 'react-dom/client'
|
|
20
|
+
import { ChakraProvider } from '@chakra-ui/react'
|
|
21
|
+
import App from './App.tsx'
|
|
22
|
+
import './index.css'
|
|
23
|
+
|
|
24
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
25
|
+
<React.StrictMode>
|
|
26
|
+
<ChakraProvider>
|
|
27
|
+
<App />
|
|
28
|
+
</ChakraProvider>
|
|
29
|
+
</React.StrictMode>,
|
|
30
|
+
)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Symptom if missing:** App renders with plain HTML, no styling, no colors, no layout.
|
|
34
|
+
|
|
35
|
+
**Fix:** Wrap `<App />` with `<ChakraProvider>`.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
10
39
|
## ⚠️ CRITICAL: Hailer App SDK vs MCP API
|
|
11
40
|
|
|
12
41
|
**IMPORTANT:** The Hailer App SDK (used in React/frontend apps) is DIFFERENT from the MCP API (used by Claude Code tools).
|
|
@@ -94,7 +123,133 @@ hailer.activity.list(
|
|
|
94
123
|
|
|
95
124
|
---
|
|
96
125
|
|
|
97
|
-
## 2.
|
|
126
|
+
## 2. Loading Workflow Schema (Field Definitions)
|
|
127
|
+
|
|
128
|
+
### ⚠️ CRITICAL: No `hailer.process.getFields()` - Use `hailer.workflow.get()`
|
|
129
|
+
|
|
130
|
+
The SDK does NOT have `hailer.process.getFields()`. This is a common mistake!
|
|
131
|
+
|
|
132
|
+
### ✅ CORRECT: Load Schema with `hailer.workflow.get()`
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// Load workflow with all field definitions
|
|
136
|
+
const workflow = await hailer.workflow.get(workflowId);
|
|
137
|
+
|
|
138
|
+
// workflow.fields is an object keyed by field ID:
|
|
139
|
+
// {
|
|
140
|
+
// "691ffdf84217e9e8434e5694": { label: "Player Name", type: "text", key: "playerName", ... },
|
|
141
|
+
// "691ffdf84217e9e8434e5695": { label: "Jersey Number", type: "numeric", key: "jerseyNumber", ... }
|
|
142
|
+
// }
|
|
143
|
+
|
|
144
|
+
// workflow.phases is an object with phase-specific field lists:
|
|
145
|
+
// {
|
|
146
|
+
// "691ffdf84217e9e8434e569f": { name: "Active", fields: ["field1", "field2", ...] }
|
|
147
|
+
// }
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Complete Schema Loading Pattern
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
154
|
+
|
|
155
|
+
interface FieldSchema {
|
|
156
|
+
_id: string;
|
|
157
|
+
key?: string;
|
|
158
|
+
label: string;
|
|
159
|
+
type: string;
|
|
160
|
+
required?: boolean;
|
|
161
|
+
data?: string[]; // For dropdown options
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function WorkflowView({ hailer, workflowId, phaseId }) {
|
|
165
|
+
const [schema, setSchema] = useState<FieldSchema[]>([]);
|
|
166
|
+
|
|
167
|
+
const loadSchema = useCallback(async () => {
|
|
168
|
+
try {
|
|
169
|
+
// ✅ CORRECT: Use hailer.workflow.get()
|
|
170
|
+
const workflowData = await hailer.workflow.get(workflowId);
|
|
171
|
+
|
|
172
|
+
if (workflowData?.fields) {
|
|
173
|
+
// Get field IDs for this phase, or all fields if phase not specified
|
|
174
|
+
const fieldIds = workflowData.phases?.[phaseId]?.fields
|
|
175
|
+
|| Object.keys(workflowData.fields);
|
|
176
|
+
|
|
177
|
+
// Convert fields object to array
|
|
178
|
+
const fieldsArray = fieldIds.map((fieldId: string) => {
|
|
179
|
+
const field = workflowData.fields[fieldId];
|
|
180
|
+
return {
|
|
181
|
+
_id: fieldId,
|
|
182
|
+
key: field?.key,
|
|
183
|
+
label: field?.label || fieldId,
|
|
184
|
+
type: field?.type || 'text',
|
|
185
|
+
required: field?.required,
|
|
186
|
+
data: field?.data, // Dropdown options
|
|
187
|
+
};
|
|
188
|
+
}).filter(f => f.label);
|
|
189
|
+
|
|
190
|
+
setSchema(fieldsArray);
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.error('Failed to load schema:', err);
|
|
194
|
+
}
|
|
195
|
+
}, [hailer, workflowId, phaseId]);
|
|
196
|
+
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
loadSchema();
|
|
199
|
+
}, [loadSchema]);
|
|
200
|
+
|
|
201
|
+
// ... rest of component
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### ❌ WRONG: These Don't Exist
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
// ❌ WRONG: hailer.process.getFields() doesn't exist!
|
|
209
|
+
await hailer.process.getFields(workflowId, phaseId);
|
|
210
|
+
|
|
211
|
+
// ❌ WRONG: hailer.workflow.getFields() doesn't exist!
|
|
212
|
+
await hailer.workflow.getFields(workflowId);
|
|
213
|
+
|
|
214
|
+
// ❌ WRONG: hailer.workflow.getSchema() doesn't exist!
|
|
215
|
+
await hailer.workflow.getSchema(workflowId);
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### SDK Workflow API Reference
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// Available workflow methods:
|
|
222
|
+
hailer.workflow.list() // List all workflows
|
|
223
|
+
hailer.workflow.get(workflowId: string) // Get single workflow with fields
|
|
224
|
+
|
|
225
|
+
// Workflow object structure:
|
|
226
|
+
interface Workflow {
|
|
227
|
+
_id: string;
|
|
228
|
+
name: string;
|
|
229
|
+
fields: {
|
|
230
|
+
[fieldId: string]: {
|
|
231
|
+
_id: string;
|
|
232
|
+
label: string;
|
|
233
|
+
type: string;
|
|
234
|
+
key?: string;
|
|
235
|
+
required?: boolean;
|
|
236
|
+
data?: string[]; // For textpredefinedoptions
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
phases: {
|
|
240
|
+
[phaseId: string]: {
|
|
241
|
+
_id: string;
|
|
242
|
+
name: string;
|
|
243
|
+
fields?: string[]; // Field IDs visible in this phase
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
phasesOrder: string[];
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## 3. Field Access - The Critical Issue
|
|
98
253
|
|
|
99
254
|
### Understanding Field Structure
|
|
100
255
|
|
|
@@ -186,7 +341,7 @@ function PlayerCard({ player }) {
|
|
|
186
341
|
|
|
187
342
|
---
|
|
188
343
|
|
|
189
|
-
##
|
|
344
|
+
## 4. Complete Working Example
|
|
190
345
|
|
|
191
346
|
### Full Component with Proper Data Loading
|
|
192
347
|
|
|
@@ -256,7 +411,7 @@ export default function PlayerList({ hailer }) {
|
|
|
256
411
|
|
|
257
412
|
---
|
|
258
413
|
|
|
259
|
-
##
|
|
414
|
+
## 5. Common Field Types
|
|
260
415
|
|
|
261
416
|
### Field Type Reference
|
|
262
417
|
|
|
@@ -307,7 +462,7 @@ console.log(team?.name);
|
|
|
307
462
|
|
|
308
463
|
---
|
|
309
464
|
|
|
310
|
-
##
|
|
465
|
+
## 6. Development Workflow
|
|
311
466
|
|
|
312
467
|
### Step-by-Step: Building a Hailer App Component
|
|
313
468
|
|
|
@@ -347,7 +502,7 @@ console.log(team?.name);
|
|
|
347
502
|
|
|
348
503
|
---
|
|
349
504
|
|
|
350
|
-
##
|
|
505
|
+
## 7. Debugging Data Loading Issues
|
|
351
506
|
|
|
352
507
|
### Quick Diagnostics
|
|
353
508
|
|
|
@@ -383,13 +538,14 @@ async function loadPlayers() {
|
|
|
383
538
|
| Issue | Symptom | Solution |
|
|
384
539
|
|-------|---------|----------|
|
|
385
540
|
| `Cannot read properties of undefined (reading 'activity')` | `hailer.api` doesn't exist | Use `hailer.activity`, NOT `hailer.api.activity` |
|
|
541
|
+
| `hailer.process.getFields is not a function` | Method doesn't exist | Use `hailer.workflow.get(workflowId)` to get fields |
|
|
386
542
|
| `"processId" must be a string` | Passing object instead of positional params | Use 3 positional params: `list(workflowId, phaseId, options)` |
|
|
387
543
|
| Fields showing as `undefined` | Using readable keys like `fields.playerName` | Use field IDs: `fields['691ffdf...']` or `getFieldValue()` |
|
|
388
544
|
| Empty data displayed | Data loads but doesn't render | Check field access - use correct field IDs |
|
|
389
545
|
|
|
390
546
|
---
|
|
391
547
|
|
|
392
|
-
##
|
|
548
|
+
## 8. Loading Data from Multiple Phases
|
|
393
549
|
|
|
394
550
|
```typescript
|
|
395
551
|
async function loadAllPlayers() {
|
|
@@ -426,7 +582,116 @@ async function loadAllPlayers() {
|
|
|
426
582
|
|
|
427
583
|
---
|
|
428
584
|
|
|
429
|
-
##
|
|
585
|
+
## 8b. Loading Activity Counts for Sidebar Navigation
|
|
586
|
+
|
|
587
|
+
When building a data manager app with a sidebar showing all workflows, you want to display activity counts immediately without requiring the user to click each workflow first.
|
|
588
|
+
|
|
589
|
+
### ✅ CORRECT: Load counts on app initialization
|
|
590
|
+
|
|
591
|
+
```typescript
|
|
592
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
593
|
+
|
|
594
|
+
export default function App() {
|
|
595
|
+
const { hailer, inside } = useHailer();
|
|
596
|
+
const [activityCounts, setActivityCounts] = useState<Record<string, number>>({});
|
|
597
|
+
const [countsLoaded, setCountsLoaded] = useState(false);
|
|
598
|
+
|
|
599
|
+
// Load activity counts for all workflows on initial load
|
|
600
|
+
useEffect(() => {
|
|
601
|
+
if (!hailer || !inside || countsLoaded) return;
|
|
602
|
+
|
|
603
|
+
async function loadWorkflowCount(workflowId: string): Promise<[string, number]> {
|
|
604
|
+
try {
|
|
605
|
+
// Get workflow to find all phases
|
|
606
|
+
const workflowData = await hailer.workflow.get(workflowId);
|
|
607
|
+
if (!workflowData?.phases) return [workflowId, 0];
|
|
608
|
+
|
|
609
|
+
// Count activities across all phases in parallel
|
|
610
|
+
const phaseIds = Object.keys(workflowData.phases);
|
|
611
|
+
const phaseCounts = await Promise.all(
|
|
612
|
+
phaseIds.map(async (phaseId) => {
|
|
613
|
+
try {
|
|
614
|
+
const activities = await hailer.activity.list(workflowId, phaseId, { limit: 500 });
|
|
615
|
+
return activities?.length || 0;
|
|
616
|
+
} catch {
|
|
617
|
+
return 0;
|
|
618
|
+
}
|
|
619
|
+
})
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
const totalCount = phaseCounts.reduce((sum, count) => sum + count, 0);
|
|
623
|
+
return [workflowId, totalCount];
|
|
624
|
+
} catch {
|
|
625
|
+
return [workflowId, 0];
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
async function loadAllCounts() {
|
|
630
|
+
// Load all workflow counts in parallel
|
|
631
|
+
const workflows = Object.values(WORKFLOWS);
|
|
632
|
+
const results = await Promise.all(
|
|
633
|
+
workflows.map((workflow) => loadWorkflowCount(workflow.id))
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
const counts: Record<string, number> = {};
|
|
637
|
+
for (const [workflowId, count] of results) {
|
|
638
|
+
counts[workflowId] = count;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
setActivityCounts(counts);
|
|
642
|
+
setCountsLoaded(true);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
loadAllCounts();
|
|
646
|
+
}, [hailer, inside, countsLoaded]);
|
|
647
|
+
|
|
648
|
+
// ... rest of component
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
### Sidebar Component with Counts
|
|
653
|
+
|
|
654
|
+
```typescript
|
|
655
|
+
interface SidebarProps {
|
|
656
|
+
selectedWorkflow: WorkflowKey | null;
|
|
657
|
+
onSelectWorkflow: (key: WorkflowKey) => void;
|
|
658
|
+
activityCounts: Record<string, number>;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export default function Sidebar({ selectedWorkflow, onSelectWorkflow, activityCounts }: SidebarProps) {
|
|
662
|
+
return (
|
|
663
|
+
<aside className="sidebar">
|
|
664
|
+
<nav className="sidebar-nav">
|
|
665
|
+
{Object.entries(WORKFLOWS).map(([key, workflow]) => (
|
|
666
|
+
<button
|
|
667
|
+
key={key}
|
|
668
|
+
className={`nav-item ${selectedWorkflow === key ? 'active' : ''}`}
|
|
669
|
+
onClick={() => onSelectWorkflow(key as WorkflowKey)}
|
|
670
|
+
>
|
|
671
|
+
<span className="nav-icon">{workflow.icon}</span>
|
|
672
|
+
<span className="nav-label">{workflow.name}</span>
|
|
673
|
+
{activityCounts[workflow.id] !== undefined && (
|
|
674
|
+
<span className="nav-count">{activityCounts[workflow.id]}</span>
|
|
675
|
+
)}
|
|
676
|
+
</button>
|
|
677
|
+
))}
|
|
678
|
+
</nav>
|
|
679
|
+
</aside>
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### Key Points
|
|
685
|
+
|
|
686
|
+
1. **Load on initialization** - Use `useEffect` to load counts when `hailer` and `inside` are ready
|
|
687
|
+
2. **Parallel loading** - Use `Promise.all` to load all workflows simultaneously
|
|
688
|
+
3. **Count all phases** - A workflow may have activities in multiple phases
|
|
689
|
+
4. **Show in sidebar** - Display counts next to workflow names
|
|
690
|
+
5. **Update on changes** - Optionally refresh counts when activities are created/updated
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
## 9. Best Practices
|
|
430
695
|
|
|
431
696
|
### ✅ DO
|
|
432
697
|
|
|
@@ -448,7 +713,7 @@ async function loadAllPlayers() {
|
|
|
448
713
|
|
|
449
714
|
---
|
|
450
715
|
|
|
451
|
-
##
|
|
716
|
+
## 10. TypeScript Definitions
|
|
452
717
|
|
|
453
718
|
```typescript
|
|
454
719
|
// src/types.ts
|
|
@@ -502,7 +767,7 @@ export interface HailerClient {
|
|
|
502
767
|
|
|
503
768
|
---
|
|
504
769
|
|
|
505
|
-
##
|
|
770
|
+
## 11. Testing Checklist
|
|
506
771
|
|
|
507
772
|
Before deploying your Hailer app:
|
|
508
773
|
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Hailer App Builder
|
|
3
|
+
description: TypeScript standards and clean code patterns for building Hailer apps - proper types, no any, error handling, and SDK patterns
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Hailer App Builder Agent
|
|
7
|
+
|
|
8
|
+
Specialized agent for writing clean, type-safe code for Hailer apps. This agent is launched after scaffolding an app.
|
|
9
|
+
|
|
10
|
+
## Agent Identity
|
|
11
|
+
|
|
12
|
+
You are a **Hailer App Builder** - a specialized TypeScript developer focused on building Hailer apps with the @hailer/app-sdk.
|
|
13
|
+
|
|
14
|
+
## TypeScript Standards (MANDATORY)
|
|
15
|
+
|
|
16
|
+
### Type Safety Rules
|
|
17
|
+
|
|
18
|
+
1. **Never use `any`** - Always define proper interfaces
|
|
19
|
+
2. **Always type function parameters and returns**
|
|
20
|
+
3. **Use generics where appropriate**
|
|
21
|
+
4. **Prefer `unknown` over `any` when type is truly unknown**
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// ❌ WRONG
|
|
25
|
+
const handleData = (data: any) => { ... }
|
|
26
|
+
const [items, setItems] = useState<any[]>([]);
|
|
27
|
+
|
|
28
|
+
// ✅ CORRECT
|
|
29
|
+
interface Activity {
|
|
30
|
+
_id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
fields: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
const handleData = (data: Activity): void => { ... }
|
|
35
|
+
const [items, setItems] = useState<Activity[]>([]);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Interface Definitions
|
|
39
|
+
|
|
40
|
+
Always define interfaces for:
|
|
41
|
+
- API responses
|
|
42
|
+
- Component props
|
|
43
|
+
- State objects
|
|
44
|
+
- Field schemas
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// Define interfaces at the top of the file or in a types.ts file
|
|
48
|
+
interface WorkflowField {
|
|
49
|
+
_id: string;
|
|
50
|
+
label: string;
|
|
51
|
+
type: 'text' | 'numeric' | 'date' | 'textpredefinedoptions' | 'activitylink' | 'users';
|
|
52
|
+
key?: string;
|
|
53
|
+
data?: string[];
|
|
54
|
+
required?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface Workflow {
|
|
58
|
+
_id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
fields: Record<string, WorkflowField>;
|
|
61
|
+
phases: Record<string, Phase>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface Phase {
|
|
65
|
+
name: string;
|
|
66
|
+
fields: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface Activity {
|
|
70
|
+
_id: string;
|
|
71
|
+
name: string;
|
|
72
|
+
fields: Record<string, FieldValue>;
|
|
73
|
+
created: number;
|
|
74
|
+
updated: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type FieldValue = string | number | string[] | null;
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Clean Code Practices
|
|
81
|
+
|
|
82
|
+
1. **Small, focused functions** - Each function does one thing
|
|
83
|
+
2. **Meaningful names** - Variables and functions describe their purpose
|
|
84
|
+
3. **Early returns** - Avoid deep nesting
|
|
85
|
+
4. **Const by default** - Only use let when reassignment is needed
|
|
86
|
+
5. **Destructure props** - Makes dependencies clear
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// ❌ WRONG
|
|
90
|
+
function doStuff(x: any) {
|
|
91
|
+
if (x) {
|
|
92
|
+
if (x.items) {
|
|
93
|
+
let result = [];
|
|
94
|
+
for (let i = 0; i < x.items.length; i++) {
|
|
95
|
+
result.push(x.items[i].name);
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ✅ CORRECT
|
|
104
|
+
function extractActivityNames(response: { items?: Activity[] } | null): string[] {
|
|
105
|
+
if (!response?.items) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
return response.items.map(item => item.name);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Hailer SDK Patterns (CRITICAL)
|
|
113
|
+
|
|
114
|
+
### Loading Workflow Schema
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// ✅ CORRECT - Use hailer.workflow.get()
|
|
118
|
+
const loadSchema = async (workflowId: string): Promise<WorkflowField[]> => {
|
|
119
|
+
const workflow = await hailer.workflow.get(workflowId);
|
|
120
|
+
|
|
121
|
+
if (!workflow?.fields) {
|
|
122
|
+
throw new Error(`Workflow ${workflowId} not found`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return Object.entries(workflow.fields).map(([id, field]) => ({
|
|
126
|
+
_id: id,
|
|
127
|
+
...field
|
|
128
|
+
}));
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// ❌ WRONG - These methods don't exist!
|
|
132
|
+
// await hailer.process.getFields(workflowId, phaseId);
|
|
133
|
+
// await hailer.workflow.getFields(workflowId);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Loading Activities
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const loadActivities = async (
|
|
140
|
+
workflowId: string,
|
|
141
|
+
phaseId: string
|
|
142
|
+
): Promise<Activity[]> => {
|
|
143
|
+
const response = await hailer.activity.list({
|
|
144
|
+
workflowId,
|
|
145
|
+
phaseId,
|
|
146
|
+
limit: 100
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return response?.items ?? [];
|
|
150
|
+
};
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Creating Activities
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
interface CreateActivityParams {
|
|
157
|
+
workflowId: string;
|
|
158
|
+
name: string;
|
|
159
|
+
fields: Record<string, FieldValue>;
|
|
160
|
+
phaseId?: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const createActivity = async (params: CreateActivityParams): Promise<Activity> => {
|
|
164
|
+
const result = await hailer.activity.create(params);
|
|
165
|
+
|
|
166
|
+
if (!result?._id) {
|
|
167
|
+
throw new Error('Failed to create activity');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return result;
|
|
171
|
+
};
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Updating Activities
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
const updateActivity = async (
|
|
178
|
+
activityId: string,
|
|
179
|
+
updates: Partial<Activity>
|
|
180
|
+
): Promise<Activity> => {
|
|
181
|
+
const result = await hailer.activity.update({
|
|
182
|
+
activityId,
|
|
183
|
+
...updates
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return result;
|
|
187
|
+
};
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## React Component Structure
|
|
191
|
+
|
|
192
|
+
### Standard Component Template
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
196
|
+
import { useHailer } from '@hailer/app-sdk';
|
|
197
|
+
|
|
198
|
+
// Types
|
|
199
|
+
interface Props {
|
|
200
|
+
workflowId: string;
|
|
201
|
+
phaseId: string;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
interface Activity {
|
|
205
|
+
_id: string;
|
|
206
|
+
name: string;
|
|
207
|
+
fields: Record<string, unknown>;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Component
|
|
211
|
+
export const ActivityList: React.FC<Props> = ({ workflowId, phaseId }) => {
|
|
212
|
+
const hailer = useHailer();
|
|
213
|
+
|
|
214
|
+
// State with proper types
|
|
215
|
+
const [activities, setActivities] = useState<Activity[]>([]);
|
|
216
|
+
const [loading, setLoading] = useState(true);
|
|
217
|
+
const [error, setError] = useState<string | null>(null);
|
|
218
|
+
|
|
219
|
+
// Memoized data loader
|
|
220
|
+
const loadData = useCallback(async () => {
|
|
221
|
+
try {
|
|
222
|
+
setLoading(true);
|
|
223
|
+
setError(null);
|
|
224
|
+
|
|
225
|
+
const response = await hailer.activity.list({
|
|
226
|
+
workflowId,
|
|
227
|
+
phaseId,
|
|
228
|
+
limit: 100
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
setActivities(response?.items ?? []);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
const message = err instanceof Error ? err.message : 'Failed to load';
|
|
234
|
+
setError(message);
|
|
235
|
+
} finally {
|
|
236
|
+
setLoading(false);
|
|
237
|
+
}
|
|
238
|
+
}, [hailer, workflowId, phaseId]);
|
|
239
|
+
|
|
240
|
+
// Load on mount
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
loadData();
|
|
243
|
+
}, [loadData]);
|
|
244
|
+
|
|
245
|
+
// Render states
|
|
246
|
+
if (loading) return <div>Loading...</div>;
|
|
247
|
+
if (error) return <div>Error: {error}</div>;
|
|
248
|
+
if (activities.length === 0) return <div>No activities found</div>;
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<ul>
|
|
252
|
+
{activities.map(activity => (
|
|
253
|
+
<li key={activity._id}>{activity.name}</li>
|
|
254
|
+
))}
|
|
255
|
+
</ul>
|
|
256
|
+
);
|
|
257
|
+
};
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## File Organization
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
src/
|
|
264
|
+
├── types/
|
|
265
|
+
│ ├── index.ts # Re-exports all types
|
|
266
|
+
│ ├── activity.ts # Activity-related types
|
|
267
|
+
│ ├── workflow.ts # Workflow/field types
|
|
268
|
+
│ └── api.ts # API response types
|
|
269
|
+
├── hooks/
|
|
270
|
+
│ ├── useActivities.ts # Activity loading hook
|
|
271
|
+
│ ├── useWorkflow.ts # Workflow schema hook
|
|
272
|
+
│ └── useForm.ts # Form state management
|
|
273
|
+
├── components/
|
|
274
|
+
│ ├── ActivityList.tsx
|
|
275
|
+
│ ├── ActivityForm.tsx
|
|
276
|
+
│ └── FieldRenderer.tsx
|
|
277
|
+
├── utils/
|
|
278
|
+
│ ├── formatters.ts # Date, number formatting
|
|
279
|
+
│ └── validators.ts # Form validation
|
|
280
|
+
└── App.tsx
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Error Handling Pattern
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
class HailerAppError extends Error {
|
|
287
|
+
constructor(
|
|
288
|
+
message: string,
|
|
289
|
+
public readonly code: string,
|
|
290
|
+
public readonly details?: unknown
|
|
291
|
+
) {
|
|
292
|
+
super(message);
|
|
293
|
+
this.name = 'HailerAppError';
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const handleApiError = (error: unknown): never => {
|
|
298
|
+
if (error instanceof Error) {
|
|
299
|
+
throw new HailerAppError(
|
|
300
|
+
error.message,
|
|
301
|
+
'API_ERROR',
|
|
302
|
+
error
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
throw new HailerAppError(
|
|
306
|
+
'Unknown error occurred',
|
|
307
|
+
'UNKNOWN_ERROR',
|
|
308
|
+
error
|
|
309
|
+
);
|
|
310
|
+
};
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Checklist Before Writing Code
|
|
314
|
+
|
|
315
|
+
Before writing any component:
|
|
316
|
+
|
|
317
|
+
1. [ ] Define all interfaces/types
|
|
318
|
+
2. [ ] Identify which SDK methods needed (workflow.get, activity.list, etc.)
|
|
319
|
+
3. [ ] Plan error handling
|
|
320
|
+
4. [ ] Consider loading states
|
|
321
|
+
5. [ ] Plan state management (local vs lifted)
|
|
322
|
+
|
|
323
|
+
## Do NOT
|
|
324
|
+
|
|
325
|
+
- ❌ Use `any` type
|
|
326
|
+
- ❌ Use `hailer.process.getFields()` (doesn't exist)
|
|
327
|
+
- ❌ Use `hailer.workflow.getFields()` (doesn't exist)
|
|
328
|
+
- ❌ Skip error handling
|
|
329
|
+
- ❌ Forget loading states
|
|
330
|
+
- ❌ Use inline types for complex objects
|
|
331
|
+
- ❌ Mix business logic with rendering
|
|
332
|
+
|
|
333
|
+
## DO
|
|
334
|
+
|
|
335
|
+
- ✅ Use `hailer.workflow.get(workflowId)` for schema
|
|
336
|
+
- ✅ Define interfaces for all data structures
|
|
337
|
+
- ✅ Handle loading/error/empty states
|
|
338
|
+
- ✅ Use proper TypeScript generics
|
|
339
|
+
- ✅ Keep components small and focused
|
|
340
|
+
- ✅ Use early returns for cleaner code
|