@hailer/mcp 1.1.12 → 1.1.13
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/CHANGELOG.md +0 -7
- package/{.claude → dist}/CLAUDE.md +2 -2
- package/dist/app.js +18 -5
- package/dist/bot/bot-config.d.ts +10 -1
- package/dist/bot/bot-config.js +64 -3
- package/dist/bot/bot-manager.d.ts +2 -0
- package/dist/bot/bot-manager.js +9 -2
- package/dist/bot/bot.d.ts +33 -0
- package/dist/bot/bot.js +461 -160
- package/dist/bot/services/message-classifier.js +17 -0
- package/dist/bot/services/permission-guard.d.ts +52 -0
- package/dist/bot/services/permission-guard.js +149 -0
- package/dist/bot/services/types.d.ts +5 -0
- package/dist/bot/services/typing-indicator.d.ts +6 -1
- package/dist/bot/services/typing-indicator.js +19 -3
- package/dist/cli.js +0 -0
- package/dist/config.d.ts +6 -1
- package/dist/config.js +43 -0
- package/dist/core.js +3 -6
- package/dist/lib/discussion-lock.d.ts +42 -0
- package/dist/lib/discussion-lock.js +110 -0
- package/dist/mcp/UserContextCache.d.ts +5 -0
- package/dist/mcp/UserContextCache.js +51 -19
- package/dist/mcp/hailer-clients.d.ts +19 -1
- package/dist/mcp/hailer-clients.js +158 -24
- package/dist/mcp/session-store.d.ts +68 -0
- package/dist/mcp/session-store.js +169 -0
- package/dist/mcp/signal-handler.js +2 -0
- package/dist/mcp/tool-registry.d.ts +17 -4
- package/dist/mcp/tool-registry.js +37 -7
- package/dist/mcp/tools/activity.js +99 -7
- package/dist/mcp/tools/app-scaffold.js +304 -336
- package/dist/mcp/tools/bot-config/constants.d.ts +23 -0
- package/dist/mcp/tools/bot-config/constants.js +94 -0
- package/dist/mcp/tools/bot-config/core.d.ts +253 -0
- package/dist/mcp/tools/bot-config/core.js +2456 -0
- package/dist/mcp/tools/bot-config/index.d.ts +10 -0
- package/dist/mcp/tools/bot-config/index.js +59 -0
- package/dist/mcp/tools/bot-config/tools.d.ts +7 -0
- package/dist/mcp/tools/bot-config/tools.js +15 -0
- package/dist/mcp/tools/bot-config/types.d.ts +50 -0
- package/dist/mcp/tools/bot-config/types.js +6 -0
- package/dist/mcp/tools/bug-fixer-tools.d.ts +45 -0
- package/dist/mcp/tools/bug-fixer-tools.js +1096 -0
- package/dist/mcp/tools/company.d.ts +9 -0
- package/dist/mcp/tools/company.js +88 -0
- package/dist/mcp/tools/discussion.js +68 -0
- package/dist/mcp/tools/document.d.ts +11 -0
- package/dist/mcp/tools/document.js +741 -0
- package/dist/mcp/tools/investigate.d.ts +9 -0
- package/dist/mcp/tools/investigate.js +254 -0
- package/dist/mcp/tools/workflow-permissions.d.ts +15 -0
- package/dist/mcp/tools/workflow-permissions.js +204 -0
- package/dist/mcp/tools/workflow.js +57 -18
- package/dist/mcp/utils/index.d.ts +2 -0
- package/dist/mcp/utils/index.js +12 -1
- package/dist/mcp/utils/role-utils.d.ts +74 -0
- package/dist/mcp/utils/role-utils.js +151 -0
- package/dist/mcp/utils/types.d.ts +43 -1
- package/dist/mcp/utils/types.js +14 -0
- package/dist/mcp/webhook-handler.d.ts +4 -0
- package/dist/mcp/webhook-handler.js +8 -0
- package/dist/mcp-server.d.ts +23 -2
- package/dist/mcp-server.js +639 -127
- package/dist/plugins/vipunen/client.d.ts +150 -0
- package/dist/plugins/vipunen/client.js +535 -0
- package/dist/plugins/vipunen/config/schema-config.json +19 -0
- package/dist/plugins/vipunen/config/schema-doc.json +22 -0
- package/dist/plugins/vipunen/index.d.ts +41 -0
- package/dist/plugins/vipunen/index.js +88 -0
- package/dist/plugins/vipunen/tools.d.ts +26 -0
- package/dist/plugins/vipunen/tools.js +501 -0
- package/dist/stdio-server.d.ts +14 -0
- package/dist/stdio-server.js +101 -0
- package/package.json +2 -1
- package/.claude/agents/agent-ada-skill-builder.md +0 -94
- package/.claude/agents/agent-alejandro-function-fields.md +0 -342
- package/.claude/agents/agent-bjorn-config-audit.md +0 -103
- package/.claude/agents/agent-builder-agent-creator.md +0 -130
- package/.claude/agents/agent-code-simplifier.md +0 -53
- package/.claude/agents/agent-dmitri-activity-crud.md +0 -159
- package/.claude/agents/agent-giuseppe-app-builder.md +0 -247
- package/.claude/agents/agent-gunther-mcp-tools.md +0 -39
- package/.claude/agents/agent-helga-workflow-config.md +0 -204
- package/.claude/agents/agent-igor-activity-mover-automation.md +0 -125
- package/.claude/agents/agent-ingrid-doc-templates.md +0 -261
- package/.claude/agents/agent-ivan-monolith.md +0 -154
- package/.claude/agents/agent-kenji-data-reader.md +0 -86
- package/.claude/agents/agent-lars-code-inspector.md +0 -102
- package/.claude/agents/agent-marco-mockup-builder.md +0 -110
- package/.claude/agents/agent-marcus-api-documenter.md +0 -323
- package/.claude/agents/agent-marketplace-publisher.md +0 -280
- package/.claude/agents/agent-marketplace-reviewer.md +0 -309
- package/.claude/agents/agent-permissions-handler.md +0 -208
- package/.claude/agents/agent-simple-writer.md +0 -48
- package/.claude/agents/agent-svetlana-code-review.md +0 -171
- package/.claude/agents/agent-tanya-test-runner.md +0 -333
- package/.claude/agents/agent-ui-designer.md +0 -100
- package/.claude/agents/agent-viktor-sql-insights.md +0 -212
- package/.claude/agents/agent-web-search.md +0 -55
- package/.claude/agents/agent-yevgeni-discussions.md +0 -45
- package/.claude/agents/agent-zara-zapier.md +0 -159
- package/.claude/commands/app-squad.md +0 -135
- package/.claude/commands/audit-squad.md +0 -158
- package/.claude/commands/autoplan.md +0 -563
- package/.claude/commands/cleanup-squad.md +0 -98
- package/.claude/commands/config-squad.md +0 -106
- package/.claude/commands/crud-squad.md +0 -87
- package/.claude/commands/data-squad.md +0 -97
- package/.claude/commands/debug-squad.md +0 -303
- package/.claude/commands/doc-squad.md +0 -65
- package/.claude/commands/handoff.md +0 -137
- package/.claude/commands/health.md +0 -49
- package/.claude/commands/help.md +0 -29
- package/.claude/commands/help:agents.md +0 -151
- package/.claude/commands/help:commands.md +0 -78
- package/.claude/commands/help:faq.md +0 -79
- package/.claude/commands/help:plugins.md +0 -50
- package/.claude/commands/help:skills.md +0 -93
- package/.claude/commands/help:tools.md +0 -75
- package/.claude/commands/hotfix-squad.md +0 -112
- package/.claude/commands/integration-squad.md +0 -82
- package/.claude/commands/janitor-squad.md +0 -167
- package/.claude/commands/learn-auto.md +0 -120
- package/.claude/commands/learn.md +0 -120
- package/.claude/commands/mcp-list.md +0 -27
- package/.claude/commands/onboard-squad.md +0 -140
- package/.claude/commands/plan-workspace.md +0 -732
- package/.claude/commands/prd.md +0 -130
- package/.claude/commands/project-status.md +0 -82
- package/.claude/commands/publish.md +0 -138
- package/.claude/commands/recap.md +0 -69
- package/.claude/commands/restore.md +0 -64
- package/.claude/commands/review-squad.md +0 -152
- package/.claude/commands/save.md +0 -24
- package/.claude/commands/stats.md +0 -19
- package/.claude/commands/swarm.md +0 -210
- package/.claude/commands/tool-builder.md +0 -39
- package/.claude/commands/ws-pull.md +0 -44
- package/.claude/hooks/_shared-memory.cjs +0 -305
- package/.claude/hooks/_utils.cjs +0 -108
- package/.claude/hooks/agent-failure-detector.cjs +0 -383
- package/.claude/hooks/agent-usage-logger.cjs +0 -204
- package/.claude/hooks/app-edit-guard.cjs +0 -494
- package/.claude/hooks/auto-learn.cjs +0 -304
- package/.claude/hooks/bash-guard.cjs +0 -272
- package/.claude/hooks/builder-mode-manager.cjs +0 -354
- package/.claude/hooks/bulk-activity-guard.cjs +0 -271
- package/.claude/hooks/context-watchdog.cjs +0 -230
- package/.claude/hooks/delegation-reminder.cjs +0 -465
- package/.claude/hooks/design-system-lint.cjs +0 -271
- package/.claude/hooks/post-scaffold-hook.cjs +0 -181
- package/.claude/hooks/prompt-guard.cjs +0 -354
- package/.claude/hooks/publish-template-guard.cjs +0 -147
- package/.claude/hooks/session-start.cjs +0 -35
- package/.claude/hooks/shared-memory-writer.cjs +0 -147
- package/.claude/hooks/skill-injector.cjs +0 -140
- package/.claude/hooks/skill-usage-logger.cjs +0 -258
- package/.claude/hooks/src-edit-guard.cjs +0 -240
- package/.claude/hooks/sync-marketplace-agents.cjs +0 -346
- package/.claude/settings.json +0 -257
- package/.claude/skills/SDK-activity-patterns/SKILL.md +0 -428
- package/.claude/skills/SDK-document-templates/SKILL.md +0 -1033
- package/.claude/skills/SDK-function-fields/SKILL.md +0 -542
- package/.claude/skills/SDK-generate-skill/SKILL.md +0 -92
- package/.claude/skills/SDK-init-skill/SKILL.md +0 -127
- package/.claude/skills/SDK-insight-queries/SKILL.md +0 -787
- package/.claude/skills/SDK-ws-config-skill/SKILL.md +0 -1139
- package/.claude/skills/agent-structure/SKILL.md +0 -98
- package/.claude/skills/api-documentation-patterns/SKILL.md +0 -474
- package/.claude/skills/chrome-mcp-reference/SKILL.md +0 -370
- package/.claude/skills/delegation-routing/SKILL.md +0 -202
- package/.claude/skills/frontend-design/SKILL.md +0 -254
- package/.claude/skills/hailer-activity-mover/SKILL.md +0 -213
- package/.claude/skills/hailer-api-client/SKILL.md +0 -518
- package/.claude/skills/hailer-app-builder/SKILL.md +0 -1434
- package/.claude/skills/hailer-apps-pictures/SKILL.md +0 -269
- package/.claude/skills/hailer-design-system/SKILL.md +0 -235
- package/.claude/skills/hailer-monolith-automations/SKILL.md +0 -686
- package/.claude/skills/hailer-permissions-system/SKILL.md +0 -121
- package/.claude/skills/hailer-project-protocol/SKILL.md +0 -488
- package/.claude/skills/hailer-rest-api/SKILL.md +0 -61
- package/.claude/skills/hailer-rest-api/hailer-activities.md +0 -184
- package/.claude/skills/hailer-rest-api/hailer-admin.md +0 -473
- package/.claude/skills/hailer-rest-api/hailer-calendar.md +0 -256
- package/.claude/skills/hailer-rest-api/hailer-feed.md +0 -249
- package/.claude/skills/hailer-rest-api/hailer-insights.md +0 -195
- package/.claude/skills/hailer-rest-api/hailer-messaging.md +0 -276
- package/.claude/skills/hailer-rest-api/hailer-workflows.md +0 -283
- package/.claude/skills/insight-join-patterns/SKILL.md +0 -174
- package/.claude/skills/integration-patterns/SKILL.md +0 -421
- package/.claude/skills/json-only-output/SKILL.md +0 -72
- package/.claude/skills/lsp-setup/SKILL.md +0 -160
- package/.claude/skills/mcp-direct-tools/SKILL.md +0 -153
- package/.claude/skills/optional-parameters/SKILL.md +0 -72
- package/.claude/skills/publish-hailer-app/SKILL.md +0 -244
- package/.claude/skills/testing-patterns/SKILL.md +0 -630
- package/.claude/skills/tool-builder/SKILL.md +0 -250
- package/.claude/skills/tool-parameter-usage/SKILL.md +0 -126
- package/.claude/skills/tool-response-verification/SKILL.md +0 -92
- package/.claude/skills/zapier-hailer-patterns/SKILL.md +0 -581
- package/.mcp.json +0 -13
- package/.opencode/agent/agent-ada-skill-builder.md +0 -35
- package/.opencode/agent/agent-alejandro-function-fields.md +0 -39
- package/.opencode/agent/agent-bjorn-config-audit.md +0 -36
- package/.opencode/agent/agent-builder-agent-creator.md +0 -39
- package/.opencode/agent/agent-code-simplifier.md +0 -31
- package/.opencode/agent/agent-dmitri-activity-crud.md +0 -40
- package/.opencode/agent/agent-giuseppe-app-builder.md +0 -37
- package/.opencode/agent/agent-gunther-mcp-tools.md +0 -39
- package/.opencode/agent/agent-helga-workflow-config.md +0 -203
- package/.opencode/agent/agent-igor-activity-mover-automation.md +0 -46
- package/.opencode/agent/agent-ingrid-doc-templates.md +0 -39
- package/.opencode/agent/agent-ivan-monolith.md +0 -46
- package/.opencode/agent/agent-kenji-data-reader.md +0 -53
- package/.opencode/agent/agent-lars-code-inspector.md +0 -28
- package/.opencode/agent/agent-marco-mockup-builder.md +0 -42
- package/.opencode/agent/agent-marcus-api-documenter.md +0 -53
- package/.opencode/agent/agent-marketplace-publisher.md +0 -44
- package/.opencode/agent/agent-marketplace-reviewer.md +0 -42
- package/.opencode/agent/agent-permissions-handler.md +0 -50
- package/.opencode/agent/agent-simple-writer.md +0 -45
- package/.opencode/agent/agent-svetlana-code-review.md +0 -39
- package/.opencode/agent/agent-tanya-test-runner.md +0 -57
- package/.opencode/agent/agent-ui-designer.md +0 -56
- package/.opencode/agent/agent-viktor-sql-insights.md +0 -34
- package/.opencode/agent/agent-web-search.md +0 -42
- package/.opencode/agent/agent-yevgeni-discussions.md +0 -37
- package/.opencode/agent/agent-zara-zapier.md +0 -53
- package/.opencode/commands/app-squad.md +0 -135
- package/.opencode/commands/audit-squad.md +0 -158
- package/.opencode/commands/autoplan.md +0 -563
- package/.opencode/commands/cleanup-squad.md +0 -98
- package/.opencode/commands/config-squad.md +0 -106
- package/.opencode/commands/crud-squad.md +0 -87
- package/.opencode/commands/data-squad.md +0 -97
- package/.opencode/commands/debug-squad.md +0 -303
- package/.opencode/commands/doc-squad.md +0 -65
- package/.opencode/commands/handoff.md +0 -137
- package/.opencode/commands/health.md +0 -49
- package/.opencode/commands/help-agents.md +0 -151
- package/.opencode/commands/help-commands.md +0 -32
- package/.opencode/commands/help-faq.md +0 -29
- package/.opencode/commands/help-plugins.md +0 -28
- package/.opencode/commands/help-skills.md +0 -7
- package/.opencode/commands/help-tools.md +0 -40
- package/.opencode/commands/help.md +0 -28
- package/.opencode/commands/hotfix-squad.md +0 -112
- package/.opencode/commands/integration-squad.md +0 -82
- package/.opencode/commands/janitor-squad.md +0 -167
- package/.opencode/commands/learn-auto.md +0 -120
- package/.opencode/commands/learn.md +0 -120
- package/.opencode/commands/mcp-list.md +0 -27
- package/.opencode/commands/onboard-squad.md +0 -140
- package/.opencode/commands/plan-workspace.md +0 -732
- package/.opencode/commands/prd.md +0 -131
- package/.opencode/commands/project-status.md +0 -82
- package/.opencode/commands/publish.md +0 -138
- package/.opencode/commands/recap.md +0 -69
- package/.opencode/commands/restore.md +0 -64
- package/.opencode/commands/review-squad.md +0 -152
- package/.opencode/commands/save.md +0 -24
- package/.opencode/commands/stats.md +0 -19
- package/.opencode/commands/swarm.md +0 -210
- package/.opencode/commands/tool-builder.md +0 -39
- package/.opencode/commands/ws-pull.md +0 -44
- package/.opencode/opencode.json +0 -28
- package/SESSION-HANDOFF.md +0 -68
- package/inbox/2026-03-04-bot-config-patterns.md +0 -24
- package/scripts/postinstall.cjs +0 -64
- package/scripts/test-hal-tools.ts +0 -154
|
@@ -1,1434 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: hailer-app-builder
|
|
3
|
-
description: Patterns for building Hailer apps with @hailer/app-sdk
|
|
4
|
-
version: 1.3.1
|
|
5
|
-
triggers:
|
|
6
|
-
- build app
|
|
7
|
-
- hailer app
|
|
8
|
-
- app sdk
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# Hailer App Builder Skill
|
|
12
|
-
|
|
13
|
-
Patterns and templates for building Hailer apps with @hailer/app-sdk.
|
|
14
|
-
|
|
15
|
-
<critical-rules>
|
|
16
|
-
## CRITICAL: Scaffolding and Data Sources
|
|
17
|
-
|
|
18
|
-
**ALWAYS use scaffold_hailer_app MCP tool** to create new apps. Never manually create the project structure.
|
|
19
|
-
|
|
20
|
-
### scaffold_hailer_app - One-Shot Full Setup
|
|
21
|
-
|
|
22
|
-
This tool does EVERYTHING in one call:
|
|
23
|
-
- Scaffolds project from template (Vite + React + TypeScript)
|
|
24
|
-
- Runs `npm install`
|
|
25
|
-
- Configures CORS in `vite.config.ts`
|
|
26
|
-
- **Creates dev app entry in Hailer** (with auto-generated icon)
|
|
27
|
-
- Shares app with entire workspace
|
|
28
|
-
- Adds appId to `manifest.json`
|
|
29
|
-
- Starts dev server on port 3000
|
|
30
|
-
|
|
31
|
-
**You're customizing a working starter app**, not building from scratch.
|
|
32
|
-
|
|
33
|
-
### create_app - Entry Only (No Local Files)
|
|
34
|
-
|
|
35
|
-
Use `mcp__hailer__create_app` when you:
|
|
36
|
-
- Need a production app entry pointing to a deployed URL
|
|
37
|
-
- Want to register an external/existing app in Hailer
|
|
38
|
-
- Already have app code and just need the Hailer entry
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
mcp__hailer__create_app({
|
|
42
|
-
name: "Production App",
|
|
43
|
-
url: "https://app.example.com"
|
|
44
|
-
})
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
**scaffold = full development setup**
|
|
48
|
-
**create_app = just the Hailer entry/frame**
|
|
49
|
-
|
|
50
|
-
**For project data structure (workflows, fields, phases):**
|
|
51
|
-
- READ workspace/ TypeScript files directly (fields.ts, phases.ts, enums.ts)
|
|
52
|
-
- Do NOT use MCP tools for data structure queries
|
|
53
|
-
- The SDK pull provides all needed type information locally
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
// For app constants, read from local workspace files:
|
|
57
|
-
// - workspace/enums.ts (IDs)
|
|
58
|
-
// - workspace/[Workflow]_[id]/fields.ts (field definitions)
|
|
59
|
-
// - workspace/[Workflow]_[id]/phases.ts (phase definitions)
|
|
60
|
-
```
|
|
61
|
-
</critical-rules>
|
|
62
|
-
|
|
63
|
-
<local-dev-flow>
|
|
64
|
-
## Development Flow
|
|
65
|
-
|
|
66
|
-
**Default: Local development.** `scaffold_hailer_app` handles everything automatically:
|
|
67
|
-
1. Creates local project files
|
|
68
|
-
2. Creates a dev app entry in Hailer at `http://localhost:3000`
|
|
69
|
-
3. Shares the app with the workspace
|
|
70
|
-
4. Starts the dev server
|
|
71
|
-
|
|
72
|
-
After scaffolding, run `npm run dev` and test inside Hailer iframe.
|
|
73
|
-
|
|
74
|
-
**Publishing: Only when user explicitly asks.** Load the `publish-hailer-app` skill, which covers manifest validation, file upload via `publish_hailer_app`, and URL switch from localhost to production via `update_app`.
|
|
75
|
-
|
|
76
|
-
### Manual Local Dev App (Rare Cases)
|
|
77
|
-
|
|
78
|
-
Only needed if:
|
|
79
|
-
- You have existing code without a dev app entry
|
|
80
|
-
- The scaffold's dev app was deleted
|
|
81
|
-
|
|
82
|
-
```
|
|
83
|
-
mcp__hailer__create_app({
|
|
84
|
-
name: "Local Dev",
|
|
85
|
-
url: "http://localhost:3000",
|
|
86
|
-
description: "Local development testing"
|
|
87
|
-
})
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
Or manually in Hailer UI: Apps → Create App → URL: http://localhost:3000
|
|
91
|
-
</local-dev-flow>
|
|
92
|
-
|
|
93
|
-
<sdk-setup>
|
|
94
|
-
## Hook Import (CRITICAL)
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
// CORRECT - local default import
|
|
98
|
-
import useHailer from './hailer/use-hailer';
|
|
99
|
-
|
|
100
|
-
// WRONG - will fail build
|
|
101
|
-
import { useHailer } from '@hailer/app-sdk';
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
## Hook Usage
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
function App() {
|
|
108
|
-
const { inside, hailer } = useHailer();
|
|
109
|
-
|
|
110
|
-
// CORRECT dependency array
|
|
111
|
-
useEffect(() => {
|
|
112
|
-
// fetch data
|
|
113
|
-
}, [inside]); // Use [inside] NOT [hailer]
|
|
114
|
-
|
|
115
|
-
// Early return AFTER hooks
|
|
116
|
-
if (!inside) return <Text>Open this app inside Hailer</Text>;
|
|
117
|
-
|
|
118
|
-
return <Box>...</Box>;
|
|
119
|
-
}
|
|
120
|
-
```
|
|
121
|
-
</sdk-setup>
|
|
122
|
-
|
|
123
|
-
<usehailer-fix>
|
|
124
|
-
## CRITICAL: Replace Scaffold's useHailer Hook
|
|
125
|
-
|
|
126
|
-
**The scaffold generates a buggy useHailer hook.** After scaffolding, ALWAYS replace `src/hailer/use-hailer.ts` with this shared-state implementation:
|
|
127
|
-
|
|
128
|
-
### Why the Scaffold's Hook is Broken
|
|
129
|
-
|
|
130
|
-
The scaffold creates a hook using per-component `useState`:
|
|
131
|
-
|
|
132
|
-
```typescript
|
|
133
|
-
// ❌ BUGGY - each component gets its own state
|
|
134
|
-
function useHailer() {
|
|
135
|
-
const [inside, setInside] = useState(false); // Each component gets separate copy!
|
|
136
|
-
|
|
137
|
-
useEffect(() => {
|
|
138
|
-
hailer.init({ config: () => setInside(true) }); // Only updates THIS component
|
|
139
|
-
}, []);
|
|
140
|
-
|
|
141
|
-
return { inside, hailer };
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
**Result:** App.tsx sees `inside: true`, but child pages (Dashboard, Settings) still see `inside: false`.
|
|
146
|
-
|
|
147
|
-
### The Fix: Shared State with useSyncExternalStore
|
|
148
|
-
|
|
149
|
-
Replace `src/hailer/use-hailer.ts` with:
|
|
150
|
-
|
|
151
|
-
```typescript
|
|
152
|
-
import { useSyncExternalStore } from 'react';
|
|
153
|
-
import HailerApi from '@hailer/app-sdk';
|
|
154
|
-
|
|
155
|
-
// Types
|
|
156
|
-
interface HailerState {
|
|
157
|
-
inside: boolean;
|
|
158
|
-
hailer: ReturnType<typeof HailerApi> | null;
|
|
159
|
-
config: Record<string, unknown> | null;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
declare global {
|
|
163
|
-
interface Window {
|
|
164
|
-
__hailerStore?: {
|
|
165
|
-
state: HailerState;
|
|
166
|
-
listeners: Set<() => void>;
|
|
167
|
-
subscribe: (listener: () => void) => () => void;
|
|
168
|
-
getSnapshot: () => HailerState;
|
|
169
|
-
setState: (newState: Partial<HailerState>) => void;
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Initialize store once on window
|
|
175
|
-
function getStore() {
|
|
176
|
-
if (!window.__hailerStore) {
|
|
177
|
-
window.__hailerStore = {
|
|
178
|
-
state: { inside: false, hailer: null, config: null },
|
|
179
|
-
listeners: new Set(),
|
|
180
|
-
subscribe(listener) {
|
|
181
|
-
this.listeners.add(listener);
|
|
182
|
-
return () => this.listeners.delete(listener);
|
|
183
|
-
},
|
|
184
|
-
getSnapshot() {
|
|
185
|
-
return this.state;
|
|
186
|
-
},
|
|
187
|
-
setState(newState) {
|
|
188
|
-
this.state = { ...this.state, ...newState };
|
|
189
|
-
this.listeners.forEach((l) => l());
|
|
190
|
-
},
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
// Initialize SDK once
|
|
194
|
-
const api = HailerApi({
|
|
195
|
-
config: (inside, cfg) => { // SDK passes (inside: boolean, config: object)
|
|
196
|
-
window.__hailerStore!.setState({
|
|
197
|
-
inside: inside,
|
|
198
|
-
config: cfg?.fields ?? null,
|
|
199
|
-
});
|
|
200
|
-
},
|
|
201
|
-
error: (err) => {
|
|
202
|
-
console.error('Hailer SDK error:', err);
|
|
203
|
-
},
|
|
204
|
-
});
|
|
205
|
-
window.__hailerStore.setState({ hailer: api });
|
|
206
|
-
}
|
|
207
|
-
return window.__hailerStore;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export default function useHailer() {
|
|
211
|
-
const store = getStore();
|
|
212
|
-
const state = useSyncExternalStore(
|
|
213
|
-
store.subscribe.bind(store),
|
|
214
|
-
store.getSnapshot.bind(store)
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
return {
|
|
218
|
-
inside: state.inside,
|
|
219
|
-
hailer: state.hailer!,
|
|
220
|
-
config: state.config,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### Why This Works
|
|
226
|
-
|
|
227
|
-
1. **Single store on `window`** - All components share one source of truth
|
|
228
|
-
2. **SDK initialized once** - No duplicate callbacks
|
|
229
|
-
3. **useSyncExternalStore** - React 18's official pattern for external state
|
|
230
|
-
4. **All components update together** - When `inside` changes, every subscriber re-renders
|
|
231
|
-
|
|
232
|
-
### Giuseppe Rule
|
|
233
|
-
|
|
234
|
-
After `scaffold_hailer_app`, ALWAYS replace `src/hailer/use-hailer.ts` with the shared-state version above.
|
|
235
|
-
</usehailer-fix>
|
|
236
|
-
|
|
237
|
-
<sdk-api>
|
|
238
|
-
## Activity API
|
|
239
|
-
|
|
240
|
-
```typescript
|
|
241
|
-
// List activities from workflow phase
|
|
242
|
-
const activities = await hailer.activity.list(workflowId, phaseId, options?);
|
|
243
|
-
|
|
244
|
-
// Get single activity
|
|
245
|
-
const activity = await hailer.activity.get(activityId);
|
|
246
|
-
|
|
247
|
-
// Create activities
|
|
248
|
-
const created = await hailer.activity.create(workflowId, activities[], options?);
|
|
249
|
-
|
|
250
|
-
// Update activities (returns count)
|
|
251
|
-
const count = await hailer.activity.update(activities[], options);
|
|
252
|
-
|
|
253
|
-
// Remove activities (returns count)
|
|
254
|
-
const count = await hailer.activity.remove(activityIds[]);
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
### ActivityListOptions
|
|
258
|
-
|
|
259
|
-
```typescript
|
|
260
|
-
interface ActivityListOptions {
|
|
261
|
-
sortBy?: 'name' | 'created' | 'updated' | 'following' | 'owner' | 'team' | 'completedOn' | 'priority';
|
|
262
|
-
sortOrder?: 'asc' | 'desc';
|
|
263
|
-
limit?: number;
|
|
264
|
-
skip?: number;
|
|
265
|
-
includeUsers?: boolean;
|
|
266
|
-
includeTeams?: boolean;
|
|
267
|
-
includeHistory?: boolean;
|
|
268
|
-
filters?: any;
|
|
269
|
-
}
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
### ActivityCreateOptions
|
|
273
|
-
|
|
274
|
-
```typescript
|
|
275
|
-
interface ActivityCreateOptions {
|
|
276
|
-
teamId?: string; // Assign to team
|
|
277
|
-
fileIds?: string[]; // Attach files (use ui.files.uploadFile first)
|
|
278
|
-
followerIds?: string[]; // Add followers
|
|
279
|
-
location?: {
|
|
280
|
-
label?: string;
|
|
281
|
-
type: 'area' | 'point' | 'polyline';
|
|
282
|
-
data: [{ lat: number; lng: number }];
|
|
283
|
-
};
|
|
284
|
-
discussionId?: string; // Link to discussion
|
|
285
|
-
phaseId?: string; // Initial phase (defaults to first)
|
|
286
|
-
returnDocument?: boolean; // Return full activity after create
|
|
287
|
-
ignoreRequired?: boolean; // Skip required field validation
|
|
288
|
-
}
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
### Activity Interface
|
|
292
|
-
|
|
293
|
-
```typescript
|
|
294
|
-
interface Activity {
|
|
295
|
-
_id: string;
|
|
296
|
-
name: string;
|
|
297
|
-
process: string;
|
|
298
|
-
currentPhase: string;
|
|
299
|
-
fields?: Record<string, { value: unknown }>;
|
|
300
|
-
files?: string[];
|
|
301
|
-
followers?: string[];
|
|
302
|
-
created?: number;
|
|
303
|
-
updated?: number;
|
|
304
|
-
updatedBy?: string;
|
|
305
|
-
priority?: number;
|
|
306
|
-
location?: {
|
|
307
|
-
type: 'point' | 'area' | 'polyline';
|
|
308
|
-
label: string | null;
|
|
309
|
-
data: Array<{ lat: number; lng: number }>;
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
### Phase Transitions (Moving Activities Between Phases)
|
|
315
|
-
|
|
316
|
-
**CRITICAL:** The SDK does NOT have a `hailer.activity.move()` method. To move activities between phases, use `hailer.activity.update()` with the `phaseId` parameter.
|
|
317
|
-
|
|
318
|
-
```typescript
|
|
319
|
-
// CORRECT - Use activity.update() with phaseId
|
|
320
|
-
await hailer.activity.update([
|
|
321
|
-
{
|
|
322
|
-
_id: activityId,
|
|
323
|
-
phaseId: newPhaseId, // Move to different phase
|
|
324
|
-
},
|
|
325
|
-
], {});
|
|
326
|
-
|
|
327
|
-
// ❌ WRONG - activity.move() DOES NOT EXIST
|
|
328
|
-
// await hailer.activity.move(activityId, newPhaseId); // This method doesn't exist in the SDK!
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
**Example: Move multiple activities to new phase**
|
|
332
|
-
```typescript
|
|
333
|
-
const activityIds = ['id1', 'id2', 'id3'];
|
|
334
|
-
const targetPhaseId = 'phaseId789';
|
|
335
|
-
|
|
336
|
-
await hailer.activity.update(
|
|
337
|
-
activityIds.map(id => ({
|
|
338
|
-
_id: id,
|
|
339
|
-
phaseId: targetPhaseId,
|
|
340
|
-
})),
|
|
341
|
-
{}
|
|
342
|
-
);
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
### CRITICAL: activity.list() Requires Valid phaseId
|
|
346
|
-
|
|
347
|
-
**Problem:** `hailer.activity.list(workflowId, '', options)` fails - empty phaseId not allowed.
|
|
348
|
-
|
|
349
|
-
**Solution:** Query all phases in parallel:
|
|
350
|
-
```typescript
|
|
351
|
-
const ALL_PHASES = ['phaseId1', 'phaseId2', 'phaseId3'];
|
|
352
|
-
|
|
353
|
-
const phaseResults = await Promise.all(
|
|
354
|
-
ALL_PHASES.map((phaseId) =>
|
|
355
|
-
hailer.activity.list(workflowId, phaseId, { limit: 500 }).catch(() => [])
|
|
356
|
-
)
|
|
357
|
-
);
|
|
358
|
-
const allActivities = phaseResults.flat();
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
### Phase Selection: Which Phases to Fetch
|
|
362
|
-
|
|
363
|
-
**Don't blindly fetch all phases.** Consider what the user needs to see:
|
|
364
|
-
|
|
365
|
-
| App Type | Phases to Fetch | Rationale |
|
|
366
|
-
|----------|-----------------|-----------|
|
|
367
|
-
| Sales dashboard | Active only | Don't show internal drafts |
|
|
368
|
-
| Product manager view | Draft + Active | Need to see work-in-progress |
|
|
369
|
-
| Archive browser | Archived only | Historical data |
|
|
370
|
-
| Kanban board | All except Archived | Full workflow visibility |
|
|
371
|
-
|
|
372
|
-
**Example: Role-based phase selection**
|
|
373
|
-
```typescript
|
|
374
|
-
// Define phases per user role
|
|
375
|
-
const PHASE_CONFIG = {
|
|
376
|
-
sales: ['activePhaseId'],
|
|
377
|
-
manager: ['draftPhaseId', 'activePhaseId'],
|
|
378
|
-
admin: ['draftPhaseId', 'activePhaseId', 'archivedPhaseId'],
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
// Fetch based on role
|
|
382
|
-
const userRole = 'sales'; // from app config or user check
|
|
383
|
-
const phases = PHASE_CONFIG[userRole] || PHASE_CONFIG.sales;
|
|
384
|
-
|
|
385
|
-
const results = await Promise.all(
|
|
386
|
-
phases.map(phaseId => hailer.activity.list(workflowId, phaseId))
|
|
387
|
-
);
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
**Document phase selection in PRD:**
|
|
391
|
-
```markdown
|
|
392
|
-
## Data Access
|
|
393
|
-
- **Product Browser**: Shows Draft + Active phases (managers need WIP visibility)
|
|
394
|
-
- **Public Dashboard**: Active only (no internal data exposed)
|
|
395
|
-
- **Insights**: Active only (accurate counts, no duplicates from drafts)
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
## Kanban API
|
|
399
|
-
|
|
400
|
-
```typescript
|
|
401
|
-
// List activities grouped by phase (kanban view)
|
|
402
|
-
const kanban = await hailer.activity.kanban.list(workflowId, options?);
|
|
403
|
-
// Returns: { map: { [phaseId]: { activities: [...], meta: { count, ... } } } }
|
|
404
|
-
|
|
405
|
-
// Load single activity in kanban format
|
|
406
|
-
const item = await hailer.activity.kanban.load(activityId);
|
|
407
|
-
// Returns: { activity: {...}, process: string, phase: string }
|
|
408
|
-
|
|
409
|
-
// Update activity priority
|
|
410
|
-
await hailer.activity.kanban.updatePriority(activityId, priority);
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
### KanbanListOptions
|
|
414
|
-
|
|
415
|
-
```typescript
|
|
416
|
-
interface ActivityKanbanListOptions {
|
|
417
|
-
filter: {
|
|
418
|
-
user?: { uid: string; field: string };
|
|
419
|
-
account?: string;
|
|
420
|
-
team?: string;
|
|
421
|
-
dates?: { field: string; start: number; end: number };
|
|
422
|
-
};
|
|
423
|
-
search?: string;
|
|
424
|
-
limit?: number;
|
|
425
|
-
skip?: number;
|
|
426
|
-
phase?: string;
|
|
427
|
-
includeUsers?: boolean;
|
|
428
|
-
includeTeams?: boolean;
|
|
429
|
-
}
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
## UI API
|
|
433
|
-
|
|
434
|
-
### Activity UI
|
|
435
|
-
|
|
436
|
-
```typescript
|
|
437
|
-
// Open activity in sidebar
|
|
438
|
-
await hailer.ui.activity.open(activityId, options?);
|
|
439
|
-
|
|
440
|
-
type ActivityTabTypes = 'detail' | 'discussion' | 'files' | 'location' | 'linkedFrom' | 'options';
|
|
441
|
-
|
|
442
|
-
// Open with specific tab
|
|
443
|
-
await hailer.ui.activity.open(activityId, { tab: 'files' });
|
|
444
|
-
|
|
445
|
-
// Open create form (returns created activity or null if cancelled)
|
|
446
|
-
const result = await hailer.ui.activity.create(workflowId, {
|
|
447
|
-
name?: string,
|
|
448
|
-
fields?: { [fieldId]: value },
|
|
449
|
-
location?: { type: 'point', data: [{ lat, lng }] }
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
// Bulk edit multiple activities
|
|
453
|
-
await hailer.ui.activity.editMultiple(activityIds[]);
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
**Note:** `hailer.openSidebar()` does NOT exist - use `hailer.ui.activity.open()`.
|
|
457
|
-
|
|
458
|
-
### File Upload
|
|
459
|
-
|
|
460
|
-
```typescript
|
|
461
|
-
// Upload file to Hailer (returns file ID)
|
|
462
|
-
const fileId = await hailer.ui.files.uploadFile(
|
|
463
|
-
file, // File object from <input type="file">
|
|
464
|
-
filename, // Desired filename
|
|
465
|
-
{ isPublic?: boolean }
|
|
466
|
-
);
|
|
467
|
-
|
|
468
|
-
// Then attach to activity on create:
|
|
469
|
-
await hailer.activity.create(workflowId, [{ name: 'Doc' }], { fileIds: [fileId] });
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
### Snackbar (Toast Notifications)
|
|
473
|
-
|
|
474
|
-
```typescript
|
|
475
|
-
// Show notification
|
|
476
|
-
await hailer.ui.snackbar.open(text, buttonLabel, duration?);
|
|
477
|
-
|
|
478
|
-
// Examples
|
|
479
|
-
hailer.ui.snackbar.open('Saved!', 'OK');
|
|
480
|
-
hailer.ui.snackbar.open('Deleted', 'Undo', 5000);
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
### Insight UI
|
|
484
|
-
|
|
485
|
-
```typescript
|
|
486
|
-
await hailer.ui.insight.create(workspaceId?); // Open create dialog
|
|
487
|
-
await hailer.ui.insight.edit(insightId); // Open edit dialog
|
|
488
|
-
await hailer.ui.insight.delete(insightId); // Open delete confirmation
|
|
489
|
-
await hailer.ui.insight.permission(insightId); // Open permissions dialog
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
## Insight API
|
|
493
|
-
|
|
494
|
-
```typescript
|
|
495
|
-
// Get insight data (SQL query results)
|
|
496
|
-
const data = await hailer.insight.data(insightId, { update?: true });
|
|
497
|
-
|
|
498
|
-
// List all insights
|
|
499
|
-
const insights = await hailer.insight.list();
|
|
500
|
-
|
|
501
|
-
// Update insight
|
|
502
|
-
const updated = await hailer.insight.update(insightId, partialUpdate);
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
**Response structure:**
|
|
506
|
-
```typescript
|
|
507
|
-
interface InsightData {
|
|
508
|
-
columns: string[];
|
|
509
|
-
rows: any[][];
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
interface InsightDoc {
|
|
513
|
-
_id: string;
|
|
514
|
-
name: string;
|
|
515
|
-
// ... other insight properties
|
|
516
|
-
}
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
## Workflow API
|
|
520
|
-
|
|
521
|
-
```typescript
|
|
522
|
-
// List all workflows
|
|
523
|
-
const workflows = await hailer.workflow.list();
|
|
524
|
-
|
|
525
|
-
// Get single workflow with full schema
|
|
526
|
-
const workflow = await hailer.workflow.get(workflowId);
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
## User API
|
|
530
|
-
|
|
531
|
-
```typescript
|
|
532
|
-
// Get current logged-in user
|
|
533
|
-
const me = await hailer.user.current();
|
|
534
|
-
// Returns: { _id, email, firstname, lastname, ... }
|
|
535
|
-
|
|
536
|
-
// Get specific user by ID
|
|
537
|
-
const user = await hailer.user.get(userId);
|
|
538
|
-
|
|
539
|
-
// List all workspace users (returns object keyed by user ID)
|
|
540
|
-
const users = await hailer.user.list();
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
### Getting User Info for Personalized Greeting
|
|
544
|
-
|
|
545
|
-
**The config callback does NOT include user info:**
|
|
546
|
-
```typescript
|
|
547
|
-
// ❌ WRONG - config only has { fields: {} }
|
|
548
|
-
const { config } = useHailer();
|
|
549
|
-
const userName = config?.userName; // undefined!
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
**Use hailer.user.current() instead:**
|
|
553
|
-
```typescript
|
|
554
|
-
const { inside, hailer } = useHailer();
|
|
555
|
-
const [userName, setUserName] = useState<string>('');
|
|
556
|
-
|
|
557
|
-
useEffect(() => {
|
|
558
|
-
if (!inside) return;
|
|
559
|
-
|
|
560
|
-
hailer.user.current().then(user => {
|
|
561
|
-
setUserName(user.firstname || 'there');
|
|
562
|
-
});
|
|
563
|
-
}, [inside]);
|
|
564
|
-
|
|
565
|
-
return <Heading>Hello, {userName}!</Heading>;
|
|
566
|
-
```
|
|
567
|
-
|
|
568
|
-
## Workspace API
|
|
569
|
-
|
|
570
|
-
```typescript
|
|
571
|
-
// Get current workspace
|
|
572
|
-
const workspace = await hailer.workspace.current();
|
|
573
|
-
|
|
574
|
-
// List workspaces (personal apps only)
|
|
575
|
-
const workspaces = await hailer.workspace.list();
|
|
576
|
-
```
|
|
577
|
-
</sdk-api>
|
|
578
|
-
|
|
579
|
-
<field-patterns>
|
|
580
|
-
## Extracting Field Values
|
|
581
|
-
|
|
582
|
-
```typescript
|
|
583
|
-
// Fields are optional and nested
|
|
584
|
-
interface Activity {
|
|
585
|
-
fields?: Record<string, { value: unknown }>;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// Safe extraction helper
|
|
589
|
-
function getFieldValue<T>(activity: Activity, fieldId: string, defaultValue: T): T {
|
|
590
|
-
return (activity.fields?.[fieldId]?.value as T) ?? defaultValue;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// Usage
|
|
594
|
-
const name = getFieldValue(activity, 'fieldId123', '');
|
|
595
|
-
const count = getFieldValue(activity, 'fieldId456', 0);
|
|
596
|
-
const date = getFieldValue(activity, 'fieldId789', null);
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
## Field Types
|
|
600
|
-
|
|
601
|
-
```typescript
|
|
602
|
-
// Text field
|
|
603
|
-
const text = activity.fields?.['fieldId']?.value as string;
|
|
604
|
-
|
|
605
|
-
// Number field
|
|
606
|
-
const num = activity.fields?.['fieldId']?.value as number;
|
|
607
|
-
|
|
608
|
-
// Date field (timestamp)
|
|
609
|
-
const date = activity.fields?.['fieldId']?.value as number;
|
|
610
|
-
const formatted = new Date(date).toLocaleDateString();
|
|
611
|
-
|
|
612
|
-
// Enum/Select field
|
|
613
|
-
const status = activity.fields?.['fieldId']?.value as string;
|
|
614
|
-
|
|
615
|
-
// ActivityLink field (reference to another activity)
|
|
616
|
-
// IMPORTANT: ActivityLink has nested structure, not direct values
|
|
617
|
-
interface ActivityLinkValue {
|
|
618
|
-
_id: string;
|
|
619
|
-
name: string;
|
|
620
|
-
}
|
|
621
|
-
const linked = activity.fields?.['fieldId']?.value as ActivityLinkValue;
|
|
622
|
-
const linkedId = linked?._id; // Get linked activity ID
|
|
623
|
-
const linkedName = linked?.name || 'Unknown'; // Get display name
|
|
624
|
-
|
|
625
|
-
// User field
|
|
626
|
-
interface UserValue {
|
|
627
|
-
_id: string;
|
|
628
|
-
firstname: string;
|
|
629
|
-
lastname: string;
|
|
630
|
-
}
|
|
631
|
-
const user = activity.fields?.['fieldId']?.value as UserValue;
|
|
632
|
-
const userName = user ? `${user.firstname} ${user.lastname}` : 'Unknown';
|
|
633
|
-
```
|
|
634
|
-
|
|
635
|
-
## Finnish Date Parsing
|
|
636
|
-
|
|
637
|
-
Insights and some fields return Finnish date strings (`dd.mm.yyyy`) instead of timestamps. These can't be sorted directly.
|
|
638
|
-
|
|
639
|
-
```typescript
|
|
640
|
-
// ❌ WRONG - Number("03.02.2026") returns NaN
|
|
641
|
-
dates.sort((a, b) => Number(a) - Number(b));
|
|
642
|
-
|
|
643
|
-
// ✅ CORRECT - Parse Finnish dates to Date objects
|
|
644
|
-
function parseFinnishDate(dateStr: string): Date | null {
|
|
645
|
-
// Matches "03.02.2026" or "03.02.2026 10:00"
|
|
646
|
-
const match = dateStr.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2}))?/);
|
|
647
|
-
if (!match) return null;
|
|
648
|
-
|
|
649
|
-
const [, day, month, year, hours = '0', minutes = '0'] = match;
|
|
650
|
-
return new Date(
|
|
651
|
-
parseInt(year),
|
|
652
|
-
parseInt(month) - 1, // JS months are 0-indexed
|
|
653
|
-
parseInt(day),
|
|
654
|
-
parseInt(hours),
|
|
655
|
-
parseInt(minutes)
|
|
656
|
-
);
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// Sorting Finnish dates
|
|
660
|
-
items.sort((a, b) => {
|
|
661
|
-
const dateA = parseFinnishDate(a.dateField);
|
|
662
|
-
const dateB = parseFinnishDate(b.dateField);
|
|
663
|
-
if (!dateA || !dateB) return 0;
|
|
664
|
-
return dateA.getTime() - dateB.getTime();
|
|
665
|
-
});
|
|
666
|
-
|
|
667
|
-
// Formatting back to Finnish
|
|
668
|
-
function formatFinnishDate(date: Date): string {
|
|
669
|
-
return date.toLocaleDateString('fi-FI'); // "3.2.2026"
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
function formatFinnishDateTime(date: Date): string {
|
|
673
|
-
return date.toLocaleString('fi-FI', {
|
|
674
|
-
day: 'numeric',
|
|
675
|
-
month: 'numeric',
|
|
676
|
-
year: 'numeric',
|
|
677
|
-
hour: '2-digit',
|
|
678
|
-
minute: '2-digit',
|
|
679
|
-
}); // "3.2.2026 klo 10.00"
|
|
680
|
-
}
|
|
681
|
-
```
|
|
682
|
-
|
|
683
|
-
**When to use:**
|
|
684
|
-
- Insight data returns formatted dates (not timestamps)
|
|
685
|
-
- Text fields containing dates
|
|
686
|
-
- Displaying dates to Finnish users
|
|
687
|
-
</field-patterns>
|
|
688
|
-
|
|
689
|
-
<component-templates>
|
|
690
|
-
## Activity Table
|
|
691
|
-
|
|
692
|
-
```typescript
|
|
693
|
-
import { Table, Thead, Tbody, Tr, Th, Td, Box, Spinner, Text } from '@chakra-ui/react';
|
|
694
|
-
|
|
695
|
-
interface Activity {
|
|
696
|
-
_id: string;
|
|
697
|
-
name: string;
|
|
698
|
-
fields?: Record<string, { value: unknown }>;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
interface Props {
|
|
702
|
-
activities: Activity[];
|
|
703
|
-
loading: boolean;
|
|
704
|
-
columns: { fieldId: string; label: string }[];
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
function ActivityTable({ activities, loading, columns }: Props) {
|
|
708
|
-
if (loading) return <Spinner />;
|
|
709
|
-
if (activities.length === 0) return <Text>No data</Text>;
|
|
710
|
-
|
|
711
|
-
return (
|
|
712
|
-
<Table variant="simple" size="sm">
|
|
713
|
-
<Thead>
|
|
714
|
-
<Tr>
|
|
715
|
-
<Th>Name</Th>
|
|
716
|
-
{columns.map(col => (
|
|
717
|
-
<Th key={col.fieldId}>{col.label}</Th>
|
|
718
|
-
))}
|
|
719
|
-
</Tr>
|
|
720
|
-
</Thead>
|
|
721
|
-
<Tbody>
|
|
722
|
-
{activities.map(activity => (
|
|
723
|
-
<Tr key={activity._id}>
|
|
724
|
-
<Td>{activity.name}</Td>
|
|
725
|
-
{columns.map(col => (
|
|
726
|
-
<Td key={col.fieldId}>
|
|
727
|
-
{String(activity.fields?.[col.fieldId]?.value ?? '-')}
|
|
728
|
-
</Td>
|
|
729
|
-
))}
|
|
730
|
-
</Tr>
|
|
731
|
-
))}
|
|
732
|
-
</Tbody>
|
|
733
|
-
</Table>
|
|
734
|
-
);
|
|
735
|
-
}
|
|
736
|
-
```
|
|
737
|
-
|
|
738
|
-
## Activity Card
|
|
739
|
-
|
|
740
|
-
```typescript
|
|
741
|
-
import { Box, Heading, Text, VStack, useColorModeValue } from '@chakra-ui/react';
|
|
742
|
-
|
|
743
|
-
interface Props {
|
|
744
|
-
activity: Activity;
|
|
745
|
-
fieldId: string;
|
|
746
|
-
fieldLabel: string;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
function ActivityCard({ activity, fieldId, fieldLabel }: Props) {
|
|
750
|
-
const bg = useColorModeValue('white', 'gray.700');
|
|
751
|
-
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
|
752
|
-
|
|
753
|
-
return (
|
|
754
|
-
<Box
|
|
755
|
-
p={4}
|
|
756
|
-
bg={bg}
|
|
757
|
-
borderRadius="md"
|
|
758
|
-
border="1px"
|
|
759
|
-
borderColor={borderColor}
|
|
760
|
-
>
|
|
761
|
-
<VStack align="start" spacing={2}>
|
|
762
|
-
<Heading size="sm">{activity.name}</Heading>
|
|
763
|
-
<Text fontSize="sm" color="gray.500">
|
|
764
|
-
{fieldLabel}: {String(activity.fields?.[fieldId]?.value ?? '-')}
|
|
765
|
-
</Text>
|
|
766
|
-
</VStack>
|
|
767
|
-
</Box>
|
|
768
|
-
);
|
|
769
|
-
}
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
## Stats Card
|
|
773
|
-
|
|
774
|
-
```typescript
|
|
775
|
-
import { Stat, StatLabel, StatNumber, StatHelpText, Box, useColorModeValue } from '@chakra-ui/react';
|
|
776
|
-
|
|
777
|
-
interface Props {
|
|
778
|
-
label: string;
|
|
779
|
-
value: number | string;
|
|
780
|
-
helpText?: string;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
function StatsCard({ label, value, helpText }: Props) {
|
|
784
|
-
const bg = useColorModeValue('white', 'gray.700');
|
|
785
|
-
|
|
786
|
-
return (
|
|
787
|
-
<Box p={4} bg={bg} borderRadius="md" shadow="sm">
|
|
788
|
-
<Stat>
|
|
789
|
-
<StatLabel>{label}</StatLabel>
|
|
790
|
-
<StatNumber>{value}</StatNumber>
|
|
791
|
-
{helpText && <StatHelpText>{helpText}</StatHelpText>}
|
|
792
|
-
</Stat>
|
|
793
|
-
</Box>
|
|
794
|
-
);
|
|
795
|
-
}
|
|
796
|
-
```
|
|
797
|
-
</component-templates>
|
|
798
|
-
|
|
799
|
-
<app-template>
|
|
800
|
-
## Full App Template
|
|
801
|
-
|
|
802
|
-
```typescript
|
|
803
|
-
import { useEffect, useState } from 'react';
|
|
804
|
-
import {
|
|
805
|
-
Box,
|
|
806
|
-
Heading,
|
|
807
|
-
Text,
|
|
808
|
-
Spinner,
|
|
809
|
-
Table,
|
|
810
|
-
Thead,
|
|
811
|
-
Tbody,
|
|
812
|
-
Tr,
|
|
813
|
-
Th,
|
|
814
|
-
Td,
|
|
815
|
-
VStack,
|
|
816
|
-
useColorModeValue,
|
|
817
|
-
} from '@chakra-ui/react';
|
|
818
|
-
import useHailer from './hailer/use-hailer';
|
|
819
|
-
|
|
820
|
-
// Field IDs from workflow schema (provided by orchestrator)
|
|
821
|
-
const FIELDS = {
|
|
822
|
-
NAME_FIELD: 'fieldId123',
|
|
823
|
-
STATUS_FIELD: 'fieldId456',
|
|
824
|
-
} as const;
|
|
825
|
-
|
|
826
|
-
interface Activity {
|
|
827
|
-
_id: string;
|
|
828
|
-
name: string;
|
|
829
|
-
fields?: Record<string, { value: unknown }>;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
function App() {
|
|
833
|
-
const { inside, hailer } = useHailer();
|
|
834
|
-
const [activities, setActivities] = useState<Activity[]>([]);
|
|
835
|
-
const [loading, setLoading] = useState(true);
|
|
836
|
-
const [error, setError] = useState<string | null>(null);
|
|
837
|
-
|
|
838
|
-
const bg = useColorModeValue('gray.50', 'gray.800');
|
|
839
|
-
|
|
840
|
-
// Fetch data when inside Hailer
|
|
841
|
-
useEffect(() => {
|
|
842
|
-
if (!inside) return;
|
|
843
|
-
|
|
844
|
-
async function fetchData() {
|
|
845
|
-
try {
|
|
846
|
-
setLoading(true);
|
|
847
|
-
const data = await hailer.activity.list(
|
|
848
|
-
'workflowId', // Replace with actual workflow ID
|
|
849
|
-
'phaseId', // Replace with actual phase ID
|
|
850
|
-
{ limit: 100 }
|
|
851
|
-
);
|
|
852
|
-
setActivities(data);
|
|
853
|
-
} catch (err) {
|
|
854
|
-
setError(err instanceof Error ? err.message : 'Failed to load data');
|
|
855
|
-
} finally {
|
|
856
|
-
setLoading(false);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
fetchData();
|
|
861
|
-
}, [inside]); // IMPORTANT: [inside] not [hailer]
|
|
862
|
-
|
|
863
|
-
// Early return AFTER hooks
|
|
864
|
-
if (!inside) {
|
|
865
|
-
return (
|
|
866
|
-
<Box p={8} textAlign="center">
|
|
867
|
-
<Text>Please open this app inside Hailer</Text>
|
|
868
|
-
</Box>
|
|
869
|
-
);
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
if (loading) {
|
|
873
|
-
return (
|
|
874
|
-
<Box p={8} textAlign="center">
|
|
875
|
-
<Spinner size="xl" />
|
|
876
|
-
</Box>
|
|
877
|
-
);
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
if (error) {
|
|
881
|
-
return (
|
|
882
|
-
<Box p={8} textAlign="center">
|
|
883
|
-
<Text color="red.500">{error}</Text>
|
|
884
|
-
</Box>
|
|
885
|
-
);
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
return (
|
|
889
|
-
<Box p={4} bg={bg} minH="100vh">
|
|
890
|
-
<VStack spacing={4} align="stretch">
|
|
891
|
-
<Heading size="lg">Dashboard</Heading>
|
|
892
|
-
|
|
893
|
-
<Table variant="simple" size="sm">
|
|
894
|
-
<Thead>
|
|
895
|
-
<Tr>
|
|
896
|
-
<Th>Name</Th>
|
|
897
|
-
<Th>Status</Th>
|
|
898
|
-
</Tr>
|
|
899
|
-
</Thead>
|
|
900
|
-
<Tbody>
|
|
901
|
-
{activities.map(activity => (
|
|
902
|
-
<Tr key={activity._id}>
|
|
903
|
-
<Td>{activity.name}</Td>
|
|
904
|
-
<Td>{String(activity.fields?.[FIELDS.STATUS_FIELD]?.value ?? '-')}</Td>
|
|
905
|
-
</Tr>
|
|
906
|
-
))}
|
|
907
|
-
</Tbody>
|
|
908
|
-
</Table>
|
|
909
|
-
</VStack>
|
|
910
|
-
</Box>
|
|
911
|
-
);
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
export default App;
|
|
915
|
-
```
|
|
916
|
-
</app-template>
|
|
917
|
-
|
|
918
|
-
<theme-patterns>
|
|
919
|
-
## Hailer Theme Colors
|
|
920
|
-
|
|
921
|
-
```typescript
|
|
922
|
-
// Dark mode support - ALWAYS use useColorModeValue
|
|
923
|
-
const bg = useColorModeValue('white', 'gray.700');
|
|
924
|
-
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
|
925
|
-
const textColor = useColorModeValue('gray.800', 'white');
|
|
926
|
-
const mutedColor = useColorModeValue('gray.500', 'gray.400');
|
|
927
|
-
|
|
928
|
-
// Valid color tokens (DO NOT invent tokens)
|
|
929
|
-
// gray.50, gray.100, ..., gray.900
|
|
930
|
-
// white, black
|
|
931
|
-
// red.500, green.500, blue.500, yellow.500, purple.500
|
|
932
|
-
```
|
|
933
|
-
|
|
934
|
-
## Light/Dark Mode Safety Rules
|
|
935
|
-
|
|
936
|
-
**DON'T use these patterns:**
|
|
937
|
-
```typescript
|
|
938
|
-
// ❌ WRONG - brand colors may not be defined in theme
|
|
939
|
-
color="brand.600"
|
|
940
|
-
|
|
941
|
-
// ❌ WRONG - hard-coded white fails on light backgrounds
|
|
942
|
-
<Text color="white">Always white</Text>
|
|
943
|
-
|
|
944
|
-
// ❌ WRONG - assumes light mode background
|
|
945
|
-
<Badge bg="blue.100" color="blue.800">Status</Badge>
|
|
946
|
-
```
|
|
947
|
-
|
|
948
|
-
**DO use these patterns:**
|
|
949
|
-
```typescript
|
|
950
|
-
// ✅ CORRECT - explicit colors that exist in Chakra
|
|
951
|
-
color="blue.600"
|
|
952
|
-
|
|
953
|
-
// ✅ CORRECT - adapts to color mode
|
|
954
|
-
<Text color={useColorModeValue('gray.800', 'white')}>Adapts</Text>
|
|
955
|
-
|
|
956
|
-
// ✅ CORRECT - badge adapts to mode
|
|
957
|
-
const badgeBg = useColorModeValue('blue.100', 'blue.700');
|
|
958
|
-
const badgeColor = useColorModeValue('blue.800', 'blue.100');
|
|
959
|
-
<Badge bg={badgeBg} color={badgeColor}>Status</Badge>
|
|
960
|
-
```
|
|
961
|
-
|
|
962
|
-
**Semantic tokens (define once, use everywhere):**
|
|
963
|
-
```typescript
|
|
964
|
-
// In theme.ts extendTheme
|
|
965
|
-
semanticTokens: {
|
|
966
|
-
colors: {
|
|
967
|
-
appBg: { _light: 'gray.50', _dark: 'gray.800' },
|
|
968
|
-
cardBg: { _light: 'white', _dark: 'gray.700' },
|
|
969
|
-
textPrimary: { _light: 'gray.800', _dark: 'white' },
|
|
970
|
-
textMuted: { _light: 'gray.500', _dark: 'gray.400' },
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
// Usage - no useColorModeValue needed
|
|
975
|
-
<Box bg="appBg"><Text color="textPrimary">Clean!</Text></Box>
|
|
976
|
-
```
|
|
977
|
-
|
|
978
|
-
## Phase-Colored Badges and Bars
|
|
979
|
-
|
|
980
|
-
When displaying phase/status colors that need to work in both modes:
|
|
981
|
-
|
|
982
|
-
```typescript
|
|
983
|
-
import { useColorMode } from '@chakra-ui/react';
|
|
984
|
-
|
|
985
|
-
// Helper for phase-colored elements
|
|
986
|
-
function usePhaseColors(baseColor: string) {
|
|
987
|
-
const { colorMode } = useColorMode();
|
|
988
|
-
|
|
989
|
-
if (colorMode === 'light') {
|
|
990
|
-
return {
|
|
991
|
-
bg: `${baseColor}.100`,
|
|
992
|
-
color: `${baseColor}.800`,
|
|
993
|
-
borderColor: `${baseColor}.200`,
|
|
994
|
-
};
|
|
995
|
-
} else {
|
|
996
|
-
return {
|
|
997
|
-
bg: `${baseColor}.700`,
|
|
998
|
-
color: `${baseColor}.100`,
|
|
999
|
-
borderColor: `${baseColor}.600`,
|
|
1000
|
-
};
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
// Usage
|
|
1005
|
-
function PhaseBadge({ phase, color }: { phase: string; color: string }) {
|
|
1006
|
-
const colors = usePhaseColors(color);
|
|
1007
|
-
|
|
1008
|
-
return (
|
|
1009
|
-
<Badge bg={colors.bg} color={colors.color}>
|
|
1010
|
-
{phase}
|
|
1011
|
-
</Badge>
|
|
1012
|
-
);
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
// Event bar with phase color
|
|
1016
|
-
function EventBar({ title, phaseColor }: { title: string; phaseColor: string }) {
|
|
1017
|
-
const colors = usePhaseColors(phaseColor);
|
|
1018
|
-
|
|
1019
|
-
return (
|
|
1020
|
-
<Box
|
|
1021
|
-
px={2}
|
|
1022
|
-
py={1}
|
|
1023
|
-
bg={colors.bg}
|
|
1024
|
-
color={colors.color}
|
|
1025
|
-
borderLeft="3px solid"
|
|
1026
|
-
borderLeftColor={colors.borderColor}
|
|
1027
|
-
borderRadius="sm"
|
|
1028
|
-
>
|
|
1029
|
-
{title}
|
|
1030
|
-
</Box>
|
|
1031
|
-
);
|
|
1032
|
-
}
|
|
1033
|
-
```
|
|
1034
|
-
|
|
1035
|
-
**Color mapping from Hailer phases:**
|
|
1036
|
-
```typescript
|
|
1037
|
-
// Map Hailer phase colors to Chakra color names
|
|
1038
|
-
const PHASE_COLOR_MAP: Record<string, string> = {
|
|
1039
|
-
'blue': 'blue',
|
|
1040
|
-
'green': 'green',
|
|
1041
|
-
'red': 'red',
|
|
1042
|
-
'yellow': 'yellow',
|
|
1043
|
-
'purple': 'purple',
|
|
1044
|
-
'orange': 'orange',
|
|
1045
|
-
'gray': 'gray',
|
|
1046
|
-
};
|
|
1047
|
-
```
|
|
1048
|
-
|
|
1049
|
-
## Common UI Patterns
|
|
1050
|
-
|
|
1051
|
-
```typescript
|
|
1052
|
-
// Page container
|
|
1053
|
-
<Box p={4} bg={useColorModeValue('gray.50', 'gray.800')} minH="100vh">
|
|
1054
|
-
|
|
1055
|
-
// Card
|
|
1056
|
-
<Box p={4} bg={useColorModeValue('white', 'gray.700')} borderRadius="md" shadow="sm">
|
|
1057
|
-
|
|
1058
|
-
// Section with border
|
|
1059
|
-
<Box p={4} border="1px" borderColor={useColorModeValue('gray.200', 'gray.600')} borderRadius="md">
|
|
1060
|
-
```
|
|
1061
|
-
</theme-patterns>
|
|
1062
|
-
|
|
1063
|
-
<troubleshooting>
|
|
1064
|
-
## Workflow Permission Errors
|
|
1065
|
-
|
|
1066
|
-
When SDK calls fail with permission/not-allowed errors, check **workflow configuration in Hailer** first.
|
|
1067
|
-
|
|
1068
|
-
**Common symptoms:**
|
|
1069
|
-
- `hailer.activity.move()` fails with permission error
|
|
1070
|
-
- Phase transitions not working
|
|
1071
|
-
- "Not allowed" errors on operations that should work
|
|
1072
|
-
|
|
1073
|
-
**Cause:** Features like phase transitions must be **enabled in workflow configuration** in Hailer. The SDK can't do operations that aren't configured.
|
|
1074
|
-
|
|
1075
|
-
**Debug steps:**
|
|
1076
|
-
1. Check Hailer UI: Workflow Settings → Phase settings
|
|
1077
|
-
2. Verify phase transitions are configured (`possibleNextPhase`)
|
|
1078
|
-
3. Verify user has permission to the workflow/phase
|
|
1079
|
-
4. Check if the feature (move, archive, etc.) is enabled for that phase
|
|
1080
|
-
|
|
1081
|
-
**Example:** Phase move fails because `possibleNextPhase` doesn't include the target phase.
|
|
1082
|
-
</troubleshooting>
|
|
1083
|
-
|
|
1084
|
-
<build-fixes>
|
|
1085
|
-
## Common Build Errors
|
|
1086
|
-
|
|
1087
|
-
### Cannot find module '@hailer/app-sdk'
|
|
1088
|
-
```typescript
|
|
1089
|
-
// WRONG
|
|
1090
|
-
import { useHailer } from '@hailer/app-sdk';
|
|
1091
|
-
|
|
1092
|
-
// CORRECT
|
|
1093
|
-
import useHailer from './hailer/use-hailer';
|
|
1094
|
-
```
|
|
1095
|
-
|
|
1096
|
-
### has no exported member 'useHailer'
|
|
1097
|
-
```typescript
|
|
1098
|
-
// WRONG - named import
|
|
1099
|
-
import { useHailer } from './hailer/use-hailer';
|
|
1100
|
-
|
|
1101
|
-
// CORRECT - default import
|
|
1102
|
-
import useHailer from './hailer/use-hailer';
|
|
1103
|
-
```
|
|
1104
|
-
|
|
1105
|
-
### fields possibly undefined
|
|
1106
|
-
```typescript
|
|
1107
|
-
// WRONG
|
|
1108
|
-
const value = activity.fields[fieldId].value;
|
|
1109
|
-
|
|
1110
|
-
// CORRECT
|
|
1111
|
-
const value = activity.fields?.[fieldId]?.value;
|
|
1112
|
-
```
|
|
1113
|
-
|
|
1114
|
-
### Infinite re-render loop
|
|
1115
|
-
```typescript
|
|
1116
|
-
// WRONG - hailer changes every render
|
|
1117
|
-
useEffect(() => { ... }, [hailer]);
|
|
1118
|
-
|
|
1119
|
-
// CORRECT - inside is stable
|
|
1120
|
-
useEffect(() => { ... }, [inside]);
|
|
1121
|
-
```
|
|
1122
|
-
|
|
1123
|
-
### Hooks order error
|
|
1124
|
-
```typescript
|
|
1125
|
-
// WRONG - early return before hook
|
|
1126
|
-
if (!inside) return <Text>Error</Text>;
|
|
1127
|
-
const [data, setData] = useState([]); // Error!
|
|
1128
|
-
|
|
1129
|
-
// CORRECT - hooks first, then early return
|
|
1130
|
-
const [data, setData] = useState([]);
|
|
1131
|
-
if (!inside) return <Text>Error</Text>;
|
|
1132
|
-
```
|
|
1133
|
-
|
|
1134
|
-
### Hooks inside map() or conditionals
|
|
1135
|
-
```typescript
|
|
1136
|
-
// WRONG - hook in map
|
|
1137
|
-
{items.map((item) => (
|
|
1138
|
-
<Tr _hover={{ bg: useColorModeValue('gray.50', 'gray.600') }}>
|
|
1139
|
-
))}
|
|
1140
|
-
|
|
1141
|
-
// CORRECT - hook at top level
|
|
1142
|
-
const hoverBg = useColorModeValue('gray.50', 'gray.600');
|
|
1143
|
-
{items.map((item) => (
|
|
1144
|
-
<Tr _hover={{ bg: hoverBg }}>
|
|
1145
|
-
))}
|
|
1146
|
-
```
|
|
1147
|
-
|
|
1148
|
-
### hailer.activity.move is not a function
|
|
1149
|
-
```typescript
|
|
1150
|
-
// ❌ WRONG - activity.move() DOES NOT EXIST (common mistake)
|
|
1151
|
-
// await hailer.activity.move(activityId, newPhaseId); // This will fail!
|
|
1152
|
-
|
|
1153
|
-
// CORRECT - use activity.update() with phaseId
|
|
1154
|
-
await hailer.activity.update([
|
|
1155
|
-
{
|
|
1156
|
-
_id: activityId,
|
|
1157
|
-
phaseId: newPhaseId,
|
|
1158
|
-
},
|
|
1159
|
-
], {});
|
|
1160
|
-
```
|
|
1161
|
-
|
|
1162
|
-
**Explanation:** Phase transitions in Hailer are done via the `update()` method by setting the `phaseId` field. There is no separate `move()` method in the SDK.
|
|
1163
|
-
|
|
1164
|
-
### Routing: HashRouter vs BrowserRouter
|
|
1165
|
-
|
|
1166
|
-
**Most Hailer apps don't need routing at all.** Use state-based page switching:
|
|
1167
|
-
```typescript
|
|
1168
|
-
const [page, setPage] = useState('home');
|
|
1169
|
-
{page === 'home' && <HomePage />}
|
|
1170
|
-
{page === 'settings' && <SettingsPage />}
|
|
1171
|
-
```
|
|
1172
|
-
|
|
1173
|
-
**If you need routing** (bookmarkable URLs, back button, public apps):
|
|
1174
|
-
```typescript
|
|
1175
|
-
// ✅ USE HashRouter - works in iframe, no server config
|
|
1176
|
-
import { createHashRouter, RouterProvider } from 'react-router-dom';
|
|
1177
|
-
|
|
1178
|
-
const router = createHashRouter([
|
|
1179
|
-
{ path: '/', element: <HomePage /> },
|
|
1180
|
-
{ path: '/settings', element: <SettingsPage /> },
|
|
1181
|
-
]);
|
|
1182
|
-
|
|
1183
|
-
// URLs look like: yourapp.com/#/settings
|
|
1184
|
-
```
|
|
1185
|
-
|
|
1186
|
-
```typescript
|
|
1187
|
-
// ⚠️ AVOID BrowserRouter in iframe apps
|
|
1188
|
-
// Can cause "No routes matched location /index.html" errors
|
|
1189
|
-
// because iframe URL contains /index.html path
|
|
1190
|
-
```
|
|
1191
|
-
|
|
1192
|
-
**When to use routing:**
|
|
1193
|
-
- Public apps with shareable URLs
|
|
1194
|
-
- Complex multi-section apps (like hailer-admin)
|
|
1195
|
-
- Apps where back button navigation matters
|
|
1196
|
-
</build-fixes>
|
|
1197
|
-
|
|
1198
|
-
<sdk-crud>
|
|
1199
|
-
## SDK Create/Update Formats
|
|
1200
|
-
|
|
1201
|
-
### activity.create() Format
|
|
1202
|
-
|
|
1203
|
-
**Signature:** `hailer.activity.create(workflowId, activities[], options)`
|
|
1204
|
-
|
|
1205
|
-
**CRITICAL:** Takes array of activities, raw field values (not wrapped).
|
|
1206
|
-
|
|
1207
|
-
```typescript
|
|
1208
|
-
await hailer.activity.create(WORKFLOW_ID, [
|
|
1209
|
-
{
|
|
1210
|
-
name: 'Activity name',
|
|
1211
|
-
fields: {
|
|
1212
|
-
[FIELD_ID]: 'string or number value', // NOT wrapped in { value: ... }
|
|
1213
|
-
[ACTIVITYLINK_FIELD]: 'linkedActivityId', // Just the ID, not { _id, name }
|
|
1214
|
-
},
|
|
1215
|
-
},
|
|
1216
|
-
], {});
|
|
1217
|
-
```
|
|
1218
|
-
|
|
1219
|
-
**Common mistakes:**
|
|
1220
|
-
- Passing single object instead of array
|
|
1221
|
-
- Wrapping field values in `{ value: ... }`
|
|
1222
|
-
- Passing `{ _id, name }` for activitylinks instead of just ID
|
|
1223
|
-
|
|
1224
|
-
### activity.update() Format
|
|
1225
|
-
|
|
1226
|
-
**Signature:** `hailer.activity.update(activities[], options)`
|
|
1227
|
-
|
|
1228
|
-
```typescript
|
|
1229
|
-
await hailer.activity.update([
|
|
1230
|
-
{
|
|
1231
|
-
_id: 'activityId',
|
|
1232
|
-
name: 'New name', // optional
|
|
1233
|
-
fields: {
|
|
1234
|
-
[FIELD_ID]: newValue,
|
|
1235
|
-
},
|
|
1236
|
-
phaseId: 'newPhaseId', // optional - move to different phase
|
|
1237
|
-
},
|
|
1238
|
-
], {});
|
|
1239
|
-
```
|
|
1240
|
-
</sdk-crud>
|
|
1241
|
-
|
|
1242
|
-
<file-structure>
|
|
1243
|
-
## Required Files
|
|
1244
|
-
|
|
1245
|
-
```
|
|
1246
|
-
src/
|
|
1247
|
-
App.tsx # Main component (EDIT THIS)
|
|
1248
|
-
main.tsx # Entry point (NEVER EDIT)
|
|
1249
|
-
hailer/
|
|
1250
|
-
use-hailer.ts # SDK hook (generated)
|
|
1251
|
-
types/
|
|
1252
|
-
index.ts # Type definitions (CREATE)
|
|
1253
|
-
utils/
|
|
1254
|
-
fields.ts # Field helpers (CREATE)
|
|
1255
|
-
constants/
|
|
1256
|
-
fields.ts # Field ID constants (CREATE)
|
|
1257
|
-
```
|
|
1258
|
-
|
|
1259
|
-
## Constants File Pattern
|
|
1260
|
-
|
|
1261
|
-
```typescript
|
|
1262
|
-
// src/constants/fields.ts
|
|
1263
|
-
export const WORKFLOW_ID = 'workflowId123';
|
|
1264
|
-
export const PHASE_ID = 'phaseId456';
|
|
1265
|
-
|
|
1266
|
-
export const FIELDS = {
|
|
1267
|
-
NAME: 'fieldId001',
|
|
1268
|
-
STATUS: 'fieldId002',
|
|
1269
|
-
DATE: 'fieldId003',
|
|
1270
|
-
} as const;
|
|
1271
|
-
```
|
|
1272
|
-
|
|
1273
|
-
## Types File Pattern
|
|
1274
|
-
|
|
1275
|
-
```typescript
|
|
1276
|
-
// src/types/index.ts
|
|
1277
|
-
export interface Activity {
|
|
1278
|
-
_id: string;
|
|
1279
|
-
name: string;
|
|
1280
|
-
fields?: Record<string, { value: unknown }>;
|
|
1281
|
-
created?: number;
|
|
1282
|
-
updated?: number;
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
export interface ActivityLinkValue {
|
|
1286
|
-
_id: string;
|
|
1287
|
-
name: string;
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
export interface UserValue {
|
|
1291
|
-
_id: string;
|
|
1292
|
-
firstname: string;
|
|
1293
|
-
lastname: string;
|
|
1294
|
-
}
|
|
1295
|
-
```
|
|
1296
|
-
</file-structure>
|
|
1297
|
-
|
|
1298
|
-
<app-manifest>
|
|
1299
|
-
## App Manifest Configuration
|
|
1300
|
-
|
|
1301
|
-
The `manifest.json` file in app root defines configurable settings exposed in Hailer UI.
|
|
1302
|
-
|
|
1303
|
-
### config.fields Structure
|
|
1304
|
-
|
|
1305
|
-
```json
|
|
1306
|
-
{
|
|
1307
|
-
"name": "My App",
|
|
1308
|
-
"version": "1.0.0",
|
|
1309
|
-
"config": {
|
|
1310
|
-
"fields": {
|
|
1311
|
-
"defaultView": {
|
|
1312
|
-
"type": "string",
|
|
1313
|
-
"label": "Default View",
|
|
1314
|
-
"default": "month"
|
|
1315
|
-
},
|
|
1316
|
-
"slotMinTime": {
|
|
1317
|
-
"type": "string",
|
|
1318
|
-
"label": "Day Start Time",
|
|
1319
|
-
"default": "08:00"
|
|
1320
|
-
},
|
|
1321
|
-
"showWeekends": {
|
|
1322
|
-
"type": "boolean",
|
|
1323
|
-
"label": "Show Weekends",
|
|
1324
|
-
"default": true
|
|
1325
|
-
},
|
|
1326
|
-
"itemsPerPage": {
|
|
1327
|
-
"type": "number",
|
|
1328
|
-
"label": "Items Per Page",
|
|
1329
|
-
"default": 25
|
|
1330
|
-
},
|
|
1331
|
-
"enabledFeatures": {
|
|
1332
|
-
"type": "array",
|
|
1333
|
-
"label": "Enabled Features",
|
|
1334
|
-
"default": ["search", "export"]
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
```
|
|
1340
|
-
|
|
1341
|
-
### Field Types
|
|
1342
|
-
|
|
1343
|
-
| Type | Description | Example Default |
|
|
1344
|
-
|------|-------------|-----------------|
|
|
1345
|
-
| `string` | Text value | `"month"` |
|
|
1346
|
-
| `number` | Numeric value | `25` |
|
|
1347
|
-
| `boolean` | True/false toggle | `true` |
|
|
1348
|
-
| `array` | List of values | `["a", "b"]` |
|
|
1349
|
-
|
|
1350
|
-
### Accessing Config in App
|
|
1351
|
-
|
|
1352
|
-
Config values come through HailerApi callback when app loads inside Hailer:
|
|
1353
|
-
|
|
1354
|
-
```typescript
|
|
1355
|
-
const { inside, hailer, config } = useHailer();
|
|
1356
|
-
|
|
1357
|
-
// Access configured values
|
|
1358
|
-
const defaultView = config?.defaultView ?? 'month';
|
|
1359
|
-
const showWeekends = config?.showWeekends ?? true;
|
|
1360
|
-
```
|
|
1361
|
-
|
|
1362
|
-
### Best Practices
|
|
1363
|
-
|
|
1364
|
-
**DO expose in config.fields:**
|
|
1365
|
-
- User-customizable options (default views, display preferences)
|
|
1366
|
-
- Workflow/phase IDs that vary per installation
|
|
1367
|
-
- Feature toggles
|
|
1368
|
-
|
|
1369
|
-
**DON'T expose in config.fields:**
|
|
1370
|
-
- Values hardcoded in app code (wastes config UI space)
|
|
1371
|
-
- Sensitive data (use environment variables)
|
|
1372
|
-
- Internal constants that users shouldn't change
|
|
1373
|
-
|
|
1374
|
-
**RULE:** If a config field is defined but the app ignores it, remove it from manifest.
|
|
1375
|
-
</app-manifest>
|
|
1376
|
-
|
|
1377
|
-
<public-api>
|
|
1378
|
-
## Public API (Apps Outside Hailer)
|
|
1379
|
-
|
|
1380
|
-
For apps that run standalone (outside Hailer iframe) without authentication:
|
|
1381
|
-
|
|
1382
|
-
```typescript
|
|
1383
|
-
// Public insight data
|
|
1384
|
-
const data = await hailer.public.insight.data(insightKey);
|
|
1385
|
-
const objects = await hailer.public.insight.dataAsObject(insightKey);
|
|
1386
|
-
|
|
1387
|
-
// Public forms
|
|
1388
|
-
const formData = await hailer.public.form.data(formsKey);
|
|
1389
|
-
const result = await hailer.public.form.submit(formsKey, formData);
|
|
1390
|
-
|
|
1391
|
-
// Public app config
|
|
1392
|
-
const config = await hailer.public.app.config();
|
|
1393
|
-
|
|
1394
|
-
// Public products
|
|
1395
|
-
const products = await hailer.public.product.list(filter?, options?);
|
|
1396
|
-
const product = await hailer.public.product.get(productId);
|
|
1397
|
-
```
|
|
1398
|
-
|
|
1399
|
-
**When to use Public API:**
|
|
1400
|
-
- Building external-facing apps (not in Hailer iframe)
|
|
1401
|
-
- Public dashboards using insight keys
|
|
1402
|
-
- Public form submissions
|
|
1403
|
-
- No user authentication available
|
|
1404
|
-
|
|
1405
|
-
**Note:** Public APIs use keys (not IDs) and don't require authentication.
|
|
1406
|
-
</public-api>
|
|
1407
|
-
|
|
1408
|
-
<sdk-reference>
|
|
1409
|
-
## SDK Type Definitions (For Edge Cases)
|
|
1410
|
-
|
|
1411
|
-
The skill covers common SDK methods. For less common APIs, read the type definitions in any Hailer app project:
|
|
1412
|
-
|
|
1413
|
-
```
|
|
1414
|
-
node_modules/@hailer/app-sdk/lib/modules/
|
|
1415
|
-
├── activity.d.ts - list, get, create, update, remove
|
|
1416
|
-
├── activity/kanban.d.ts - kanban.list, load, updatePriority
|
|
1417
|
-
├── user.d.ts - current, get, list
|
|
1418
|
-
├── ui.d.ts - snackbar, activity, insight, files
|
|
1419
|
-
├── ui/activity.d.ts - open, create, editMultiple
|
|
1420
|
-
├── ui/snackbar.d.ts - open
|
|
1421
|
-
├── ui/files.d.ts - uploadFile
|
|
1422
|
-
├── insight.d.ts - data, list, update
|
|
1423
|
-
├── process.d.ts - list, get (workflow API uses this)
|
|
1424
|
-
├── workspace.d.ts - current, list, product methods
|
|
1425
|
-
├── permission.d.ts - map
|
|
1426
|
-
├── app.d.ts - config.update, product methods
|
|
1427
|
-
└── public/ - insight, form, product (no auth)
|
|
1428
|
-
```
|
|
1429
|
-
|
|
1430
|
-
**When to check types:**
|
|
1431
|
-
- Method not documented here → read the relevant `.d.ts` file
|
|
1432
|
-
- Need method signature details → types are the source of truth
|
|
1433
|
-
- New SDK version → types show what's available
|
|
1434
|
-
</sdk-reference>
|