@majkapp/plugin-kit 3.2.1 → 3.3.1

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/docs/HOOKS.md ADDED
@@ -0,0 +1,532 @@
1
+ # Generated Hooks
2
+
3
+ The plugin-kit auto-generates React hooks from your functions. Run `npm run build` to generate hooks in `ui/src/generated/`.
4
+
5
+ ## Query Hooks (Auto-Fetch)
6
+
7
+ Query hooks automatically fetch data when the component mounts.
8
+
9
+ ```typescript
10
+ // From this function definition:
11
+ .function('health', {
12
+ description: 'Check plugin health status',
13
+ input: {
14
+ type: 'object',
15
+ properties: {},
16
+ additionalProperties: false
17
+ },
18
+ output: {
19
+ type: 'object',
20
+ properties: {
21
+ status: { type: 'string' },
22
+ timestamp: { type: 'string' }
23
+ }
24
+ },
25
+ handler: async (input, ctx) => {
26
+ return {
27
+ status: 'ok',
28
+ timestamp: new Date().toISOString()
29
+ };
30
+ }
31
+ })
32
+
33
+ // Generates this hook:
34
+ export function useHealth(
35
+ input?: HealthInput,
36
+ options?: { enabled?: boolean; refetchInterval?: number }
37
+ ): UseQueryResult<HealthOutput>
38
+ ```
39
+
40
+ ### Basic Usage
41
+
42
+ ```typescript
43
+ // ui/src/components/DashboardPage.tsx
44
+ import { useHealth } from '../generated/hooks';
45
+
46
+ export function DashboardPage() {
47
+ // Auto-fetches on mount
48
+ const { data, error, loading, refetch } = useHealth();
49
+
50
+ if (loading) return <div>Loading...</div>;
51
+ if (error) return <div>Error: {error.message}</div>;
52
+
53
+ return (
54
+ <div>
55
+ <div data-majk-id="healthStatus">{data.status}</div>
56
+ <button onClick={refetch} data-majk-id="refreshButton">
57
+ Refresh
58
+ </button>
59
+ </div>
60
+ );
61
+ }
62
+ ```
63
+
64
+ ### With Input Parameters
65
+
66
+ ```typescript
67
+ // Function with parameters
68
+ .function('getAnalytics', {
69
+ description: 'Get analytics for time period',
70
+ input: {
71
+ type: 'object',
72
+ properties: {
73
+ period: { type: 'string', enum: ['24h', '7d', '30d'] }
74
+ },
75
+ required: ['period']
76
+ },
77
+ output: { /* ... */ }
78
+ })
79
+
80
+ // Generated hook
81
+ export function useGetAnalytics(
82
+ input: GetAnalyticsInput,
83
+ options?: { enabled?: boolean; refetchInterval?: number }
84
+ ): UseQueryResult<GetAnalyticsOutput>
85
+ ```
86
+
87
+ Usage:
88
+
89
+ ```typescript
90
+ import { useGetAnalytics } from '../generated/hooks';
91
+
92
+ export function AnalyticsPage() {
93
+ // Pass input as first parameter
94
+ const { data, loading, error } = useGetAnalytics({ period: '7d' });
95
+
96
+ return (
97
+ <div data-majk-id="analyticsContainer">
98
+ {loading && <div data-majk-state="loading">Loading...</div>}
99
+ {!loading && data && (
100
+ <div data-majk-state="populated">
101
+ <div data-majk-id="activeUsersCount">{data.metrics.activeUsers}</div>
102
+ <div data-majk-id="totalSessions">{data.metrics.totalSessions}</div>
103
+ </div>
104
+ )}
105
+ </div>
106
+ );
107
+ }
108
+ ```
109
+
110
+ ### Auto-Refresh
111
+
112
+ ```typescript
113
+ export function LiveDashboard() {
114
+ // Refetch every 30 seconds
115
+ const { data } = useHealth({}, {
116
+ refetchInterval: 30000 // milliseconds
117
+ });
118
+
119
+ return <div data-majk-id="liveStatus">{data?.status}</div>;
120
+ }
121
+ ```
122
+
123
+ ### Conditional Fetching
124
+
125
+ ```typescript
126
+ export function ConditionalData() {
127
+ const [fetchEnabled, setFetchEnabled] = useState(false);
128
+
129
+ // Only fetch when enabled is true
130
+ const { data, loading } = useGetAnalytics(
131
+ { period: '7d' },
132
+ { enabled: fetchEnabled }
133
+ );
134
+
135
+ return (
136
+ <div>
137
+ <button
138
+ onClick={() => setFetchEnabled(true)}
139
+ data-majk-id="loadDataButton"
140
+ >
141
+ Load Data
142
+ </button>
143
+ {fetchEnabled && loading && <div>Loading...</div>}
144
+ {data && <div>{JSON.stringify(data)}</div>}
145
+ </div>
146
+ );
147
+ }
148
+ ```
149
+
150
+ ## Mutation Hooks (Manual Trigger)
151
+
152
+ Mutation hooks don't auto-fetch. Call `.mutate(input)` manually.
153
+
154
+ ```typescript
155
+ // From this function (mutation - modifies state):
156
+ .function('clearEvents', {
157
+ description: 'Clear all logged events',
158
+ input: {
159
+ type: 'object',
160
+ properties: {},
161
+ additionalProperties: false
162
+ },
163
+ output: {
164
+ type: 'object',
165
+ properties: {
166
+ success: { type: 'boolean' },
167
+ clearedCount: { type: 'number' }
168
+ }
169
+ },
170
+ handler: async (input, ctx) => {
171
+ const events = await ctx.storage.get('events') || [];
172
+ await ctx.storage.set('events', []);
173
+ return { success: true, clearedCount: events.length };
174
+ }
175
+ })
176
+
177
+ // Generates this hook:
178
+ export function useClearEvents(): UseMutationResult<ClearEventsInput, ClearEventsOutput>
179
+ ```
180
+
181
+ ### Basic Usage
182
+
183
+ ```typescript
184
+ import { useClearEvents } from '../generated/hooks';
185
+
186
+ export function EventsPage() {
187
+ const { mutate: clearEvents, data, loading, error } = useClearEvents();
188
+
189
+ const handleClear = async () => {
190
+ try {
191
+ const result = await clearEvents({}); // Call mutate with input
192
+ console.log(`Cleared ${result.clearedCount} events`);
193
+ } catch (err) {
194
+ console.error('Failed to clear events:', err);
195
+ }
196
+ };
197
+
198
+ return (
199
+ <button
200
+ onClick={handleClear}
201
+ disabled={loading}
202
+ data-majk-id="clearEventsButton"
203
+ data-majk-state={loading ? 'clearing' : 'idle'}
204
+ >
205
+ {loading ? 'Clearing...' : 'Clear Events'}
206
+ </button>
207
+ );
208
+ }
209
+ ```
210
+
211
+ ### With Input Parameters
212
+
213
+ ```typescript
214
+ // Function with input
215
+ .function('updateSettings', {
216
+ description: 'Update plugin settings',
217
+ input: {
218
+ type: 'object',
219
+ properties: {
220
+ theme: { type: 'string', enum: ['light', 'dark'] },
221
+ notifications: { type: 'boolean' }
222
+ },
223
+ required: ['theme']
224
+ },
225
+ output: { /* ... */ }
226
+ })
227
+ ```
228
+
229
+ Usage:
230
+
231
+ ```typescript
232
+ import { useUpdateSettings } from '../generated/hooks';
233
+
234
+ export function SettingsPage() {
235
+ const { mutate: updateSettings, loading, error } = useUpdateSettings();
236
+
237
+ const handleSave = async () => {
238
+ try {
239
+ await updateSettings({
240
+ theme: 'dark',
241
+ notifications: true
242
+ });
243
+ alert('Settings saved!');
244
+ } catch (err) {
245
+ alert(`Error: ${err.message}`);
246
+ }
247
+ };
248
+
249
+ return (
250
+ <button
251
+ onClick={handleSave}
252
+ disabled={loading}
253
+ data-majk-id="saveSettingsButton"
254
+ >
255
+ Save
256
+ </button>
257
+ );
258
+ }
259
+ ```
260
+
261
+ ### Tracking Mutation State
262
+
263
+ ```typescript
264
+ export function DeleteButton({ itemId }: { itemId: string }) {
265
+ const { mutate: deleteItem, loading, data, error } = useDeleteItem();
266
+
267
+ return (
268
+ <div>
269
+ <button
270
+ onClick={() => deleteItem({ id: itemId })}
271
+ disabled={loading}
272
+ data-majk-id="deleteButton"
273
+ data-majk-state={loading ? 'deleting' : error ? 'error' : data ? 'success' : 'idle'}
274
+ >
275
+ {loading ? 'Deleting...' : 'Delete'}
276
+ </button>
277
+
278
+ {error && (
279
+ <div data-majk-id="errorMessage" data-majk-state="error">
280
+ Error: {error.message}
281
+ </div>
282
+ )}
283
+
284
+ {data?.success && (
285
+ <div data-majk-id="successMessage" data-majk-state="success">
286
+ Deleted successfully!
287
+ </div>
288
+ )}
289
+ </div>
290
+ );
291
+ }
292
+ ```
293
+
294
+ ## uiLog Hook Pattern
295
+
296
+ **Best practice:** Use `useUiLog` hook for logging throughout your UI.
297
+
298
+ ```typescript
299
+ // Define uiLog function in plugin
300
+ .function('uiLog', {
301
+ description: 'Log messages from UI for debugging',
302
+ input: {
303
+ type: 'object',
304
+ properties: {
305
+ level: { type: 'string', enum: ['debug', 'info', 'warn', 'error'] },
306
+ component: { type: 'string' },
307
+ message: { type: 'string' },
308
+ data: { type: 'object' }
309
+ },
310
+ required: ['level', 'component', 'message']
311
+ },
312
+ output: {
313
+ type: 'object',
314
+ properties: { success: { type: 'boolean' } }
315
+ },
316
+ handler: async (input, ctx) => {
317
+ ctx.logger[input.level](`[UI:${input.component}] ${input.message}`, input.data);
318
+ return { success: true };
319
+ }
320
+ })
321
+ ```
322
+
323
+ Use in every component:
324
+
325
+ ```typescript
326
+ import { useUiLog } from '../generated/hooks';
327
+ import { useEffect } from 'react';
328
+
329
+ export function DashboardPage() {
330
+ const { mutate: uiLog } = useUiLog();
331
+
332
+ // Log lifecycle events
333
+ useEffect(() => {
334
+ uiLog({
335
+ level: 'info',
336
+ component: 'DashboardPage',
337
+ message: 'Component mounted'
338
+ });
339
+
340
+ return () => {
341
+ uiLog({
342
+ level: 'info',
343
+ component: 'DashboardPage',
344
+ message: 'Component unmounting'
345
+ });
346
+ };
347
+ }, [uiLog]);
348
+
349
+ // Log user actions
350
+ const handleButtonClick = () => {
351
+ uiLog({
352
+ level: 'info',
353
+ component: 'DashboardPage',
354
+ message: 'Button clicked',
355
+ data: { button: 'refresh', timestamp: Date.now() }
356
+ });
357
+ // ... handle click
358
+ };
359
+
360
+ // Log errors
361
+ const handleOperation = async () => {
362
+ try {
363
+ await riskyOperation();
364
+ } catch (error) {
365
+ uiLog({
366
+ level: 'error',
367
+ component: 'DashboardPage',
368
+ message: 'Operation failed',
369
+ data: {
370
+ error: error.message,
371
+ stack: error.stack
372
+ }
373
+ });
374
+ }
375
+ };
376
+
377
+ return <div>...</div>;
378
+ }
379
+ ```
380
+
381
+ ## Hook Return Types
382
+
383
+ ### UseQueryResult
384
+
385
+ ```typescript
386
+ interface UseQueryResult<T> {
387
+ data: T | undefined; // Function output (undefined until loaded)
388
+ error: Error | undefined; // Error if fetch failed
389
+ loading: boolean; // True while fetching
390
+ refetch: () => void; // Manually trigger re-fetch
391
+ }
392
+ ```
393
+
394
+ ### UseMutationResult
395
+
396
+ ```typescript
397
+ interface UseMutationResult<TInput, TOutput> {
398
+ mutate: (input: TInput) => Promise<TOutput>; // Call to execute function
399
+ data: TOutput | undefined; // Last successful result
400
+ error: Error | undefined; // Last error
401
+ loading: boolean; // True while executing
402
+ }
403
+ ```
404
+
405
+ ## Common Patterns
406
+
407
+ ### Loading States
408
+
409
+ ```typescript
410
+ export function DataDisplay() {
411
+ const { data, loading, error } = useGetData();
412
+
413
+ return (
414
+ <div data-majk-id="dataContainer" data-majk-state={
415
+ loading ? 'loading' :
416
+ error ? 'error' :
417
+ data ? 'populated' : 'empty'
418
+ }>
419
+ {loading && <Spinner data-majk-id="spinner" />}
420
+ {error && <ErrorMessage data-majk-id="error" error={error} />}
421
+ {data && <DataView data={data} />}
422
+ </div>
423
+ );
424
+ }
425
+ ```
426
+
427
+ ### Dependent Queries
428
+
429
+ ```typescript
430
+ export function UserProfile({ userId }: { userId: string }) {
431
+ // First query
432
+ const { data: user, loading: userLoading } = useGetUser({ id: userId });
433
+
434
+ // Second query depends on first
435
+ const { data: projects, loading: projectsLoading } = useGetUserProjects(
436
+ { userId },
437
+ { enabled: !!user } // Only fetch when user exists
438
+ );
439
+
440
+ if (userLoading) return <div>Loading user...</div>;
441
+ if (!user) return <div>User not found</div>;
442
+
443
+ return (
444
+ <div>
445
+ <div data-majk-id="userName">{user.name}</div>
446
+ {projectsLoading ? (
447
+ <div data-majk-state="loading">Loading projects...</div>
448
+ ) : (
449
+ <div data-majk-state="populated">
450
+ {projects?.map(p => <div key={p.id}>{p.name}</div>)}
451
+ </div>
452
+ )}
453
+ </div>
454
+ );
455
+ }
456
+ ```
457
+
458
+ ### Optimistic Updates
459
+
460
+ ```typescript
461
+ export function TodoList() {
462
+ const { data: todos, refetch } = useGetTodos();
463
+ const { mutate: completeTodo } = useCompleteTodo();
464
+
465
+ const handleComplete = async (todoId: string) => {
466
+ // Optimistically update UI
467
+ const optimisticTodos = todos?.map(t =>
468
+ t.id === todoId ? { ...t, completed: true } : t
469
+ );
470
+
471
+ // Update backend
472
+ try {
473
+ await completeTodo({ id: todoId });
474
+ refetch(); // Refetch to get source of truth
475
+ } catch (error) {
476
+ // Revert on error (or refetch)
477
+ refetch();
478
+ }
479
+ };
480
+
481
+ return <div>...</div>;
482
+ }
483
+ ```
484
+
485
+ ## Testing with Generated Hooks
486
+
487
+ Hooks are tested through UI tests:
488
+
489
+ ```typescript
490
+ // tests/plugin/ui/unit/dashboard.test.js
491
+ import { test, bot, mock } from '@majkapp/plugin-test';
492
+
493
+ test('dashboard uses hooks correctly', async () => {
494
+ const context = mock()
495
+ .withMockHandler('health', async () => ({
496
+ status: 'ok',
497
+ timestamp: new Date().toISOString()
498
+ }))
499
+ .build();
500
+
501
+ const b = await bot(context);
502
+ await b.goto('/plugin-screens/my-plugin/dashboard');
503
+
504
+ // Wait for useHealth() to fetch and render
505
+ await b.waitForSelector('[data-majk-id="healthStatus"][data-majk-state="populated"]');
506
+
507
+ const text = await b.getText('[data-majk-id="healthStatus"]');
508
+ assert.ok(text.includes('ok'));
509
+
510
+ await b.close();
511
+ });
512
+ ```
513
+
514
+ ## Hook Generation
515
+
516
+ Hooks are generated by running your build:
517
+
518
+ ```bash
519
+ npm run build
520
+ ```
521
+
522
+ Generated files (DO NOT EDIT manually):
523
+ - `ui/src/generated/hooks.ts` - React hooks
524
+ - `ui/src/generated/client.ts` - RPC client
525
+ - `ui/src/generated/types.ts` - TypeScript types
526
+ - `ui/src/generated/index.ts` - Barrel export
527
+
528
+ ## Next Steps
529
+
530
+ Run `npx @majkapp/plugin-kit --context` - ctx API for accessing MAJK
531
+ Run `npx @majkapp/plugin-kit --testing` - Test your hooks
532
+ Run `npx @majkapp/plugin-kit --config` - Build configuration