@uidu/skills 0.3.0 → 0.4.0
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/package.json +1 -1
- package/skills/uidu/SKILL.md +179 -25
- package/skills/uidu/metadata.json +1 -1
package/package.json
CHANGED
package/skills/uidu/SKILL.md
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: uidu
|
|
3
|
-
description: Use when building on the uidu platform — reading CMS pages, events, stories, donations, help center, forms; rendering WYSIWYG content; scaffolding projects; OR provisioning/authoring uidu content (create/update/delete) from the terminal via the @uidu/cli (`uidu`). Triggers on any task involving uidu, the @uidu/client SDK, @uidu/react bindings, or the uidu CLI.
|
|
3
|
+
description: Use when building on the uidu platform — a public website or an app that runs inside uidu (a custom app in a Space or workspace); reading CMS pages, events, stories, donations, help center, forms; rendering WYSIWYG content; scaffolding projects; OR provisioning/authoring uidu content (create/update/delete) from the terminal via the @uidu/cli (`uidu`). Triggers on any task involving uidu, the @uidu/client SDK, @uidu/react bindings, or the uidu CLI.
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: uidu
|
|
7
|
-
version: "0.
|
|
7
|
+
version: "0.5.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# uidu SDK
|
|
11
11
|
|
|
12
|
-
The uidu SDK is a TypeScript GraphQL toolkit for building websites and apps powered by the uidu platform. It is split into
|
|
12
|
+
The uidu SDK is a TypeScript GraphQL toolkit for building websites and apps powered by the uidu platform. It is split into three packages:
|
|
13
13
|
|
|
14
14
|
- **`@uidu/client`** — framework-agnostic GraphQL client. Safe for server-side use (RSC, route handlers, server actions, scripts).
|
|
15
|
-
- **`@uidu/
|
|
15
|
+
- **`@uidu/app-bridge`** — the browser half of a custom app: the handshake with the uidu page that frames it, and its short-lived session token. Only for apps that run inside uidu.
|
|
16
|
+
- **`@uidu/react`** — React hooks (`useUiduClient`, `useQuery`, `useFields`, `toText`, `useUiduApp`) and components (`<UiduAppProvider>`, `<PageBlocks>`, `<BlockRenderer>`, `<RichText>`, `<DynamicForm>`). That list is the **complete** public surface — there is no `useForm`, `usePage` or `useChannel`; for anything else, call a `@uidu/client` function.
|
|
16
17
|
|
|
17
18
|
`@uidu/api.js` is a legacy bridge package and is **deprecated** — use `@uidu/react` directly.
|
|
18
19
|
|
|
@@ -20,7 +21,7 @@ The uidu SDK is a TypeScript GraphQL toolkit for building websites and apps powe
|
|
|
20
21
|
|
|
21
22
|
Use this skill when the user is:
|
|
22
23
|
|
|
23
|
-
- Building a Next.js / Remix / Astro / React app powered by the uidu GraphQL API
|
|
24
|
+
- Building a Next.js / Remix / Astro / React app powered by the uidu GraphQL API — a public site, or a **custom app** that uidu shows inside a Space or workspace to signed-in members (same stack, different auth: see the rule below)
|
|
24
25
|
- Fetching CMS pages, events, blog stories, donation campaigns, or help center content
|
|
25
26
|
- Rendering uidu blocks with `<PageBlocks>` and a component map
|
|
26
27
|
- Rendering WYSIWYG body fields with `<RichText>`
|
|
@@ -34,6 +35,7 @@ Use this skill when the user is:
|
|
|
34
35
|
|---|---|---|
|
|
35
36
|
| `@uidu/client` | Server-side queries (RSC, server actions, scripts) | Framework-agnostic |
|
|
36
37
|
| `@uidu/react` | React hooks, providers, and components | Some hooks are client-only |
|
|
38
|
+
| `@uidu/app-bridge` | An app that runs inside uidu (custom app) | Browser only; in React use `<UiduAppProvider>` |
|
|
37
39
|
| `@uidu/api.js` | **Do not use for new code.** | Deprecated; re-exports `@uidu/react` |
|
|
38
40
|
|
|
39
41
|
Install:
|
|
@@ -76,18 +78,97 @@ bookings, calls, campaigns, kb-collections, kb-articles, channel` (read) and
|
|
|
76
78
|
reports `does not support <verb>` when it doesn't. Config resolves **flags → env
|
|
77
79
|
(`UIDU_*`) → `~/.uidu/config.json`** (written by `uidu login`).
|
|
78
80
|
|
|
79
|
-
##
|
|
81
|
+
## Who the app acts for — one rule
|
|
82
|
+
|
|
83
|
+
What an app *looks like* (landing page, blog, dashboard, booking tool) changes nothing.
|
|
84
|
+
What changes the code is **who the request acts for**:
|
|
80
85
|
|
|
81
86
|
| Operation | Auth | Where it runs |
|
|
82
87
|
|---|---|---|
|
|
83
|
-
| **Reads** (`get*` / `list*`, `<entity> list/get`) | `publicToken` | anywhere — server, CLI, **and the browser** |
|
|
84
|
-
| **Writes** (`create*` / `update*` / `delete*`, `<entity> create/update/delete`) | account **Bearer** (`apiKey`) | CLI and **server-side** only |
|
|
88
|
+
| **Reads** for anyone (`get*` / `list*`, `<entity> list/get`) | `publicToken` | anywhere — server, CLI, **and the browser** |
|
|
89
|
+
| **Writes** as the workspace (`create*` / `update*` / `delete*`, `<entity> create/update/delete`) | account **Bearer** (`apiKey`) | CLI and **server-side** only |
|
|
90
|
+
| **Anything as the signed-in member**, inside uidu (a custom app) | session token from the host, over `@uidu/app-bridge` | **the browser only** |
|
|
85
91
|
|
|
86
92
|
The write functions live in `@uidu/client`, so anything the CLI provisions you can also do
|
|
87
93
|
from a Next.js server action / route handler / RSC. **Never put the account Bearer token in a
|
|
88
94
|
browser client component.** Authoring mutations return a payload with an `errors` array —
|
|
89
95
|
always check it (non-empty = validation failure).
|
|
90
96
|
|
|
97
|
+
One app can mix the rows: public pages rendered on the server with `publicToken`, and a members'
|
|
98
|
+
area that only works when uidu frames it.
|
|
99
|
+
|
|
100
|
+
### Inside uidu: the third row
|
|
101
|
+
|
|
102
|
+
uidu renders a custom app in a sandboxed iframe and hands it a short-lived session (5 minutes,
|
|
103
|
+
refreshed by the bridge) over `postMessage`. That session is the member's: the app can do what
|
|
104
|
+
that person can do, and no more. It changes five things:
|
|
105
|
+
|
|
106
|
+
1. **The token only exists in the browser.** It arrives after the page loads, so a Server
|
|
107
|
+
Component or a server action never has it. Pages that show member data are `'use client'`
|
|
108
|
+
and read through the bridge-backed client. Fetching them in an RSC renders an empty page.
|
|
109
|
+
2. **The token only works from the app's own origin.** uidu checks `Origin` against the origin
|
|
110
|
+
it signed the token for. Don't forward it to your own backend to call uidu from there — that
|
|
111
|
+
needs a server credential uidu doesn't issue yet.
|
|
112
|
+
3. **Treat the token as opaque.** Never decode it. The expiry comes with it, the bridge refreshes
|
|
113
|
+
it, and `graphqlUrl` comes with it too: never build `https://<workspace>.uidu.org` from a slug.
|
|
114
|
+
4. **Data goes in the app's own Models.** The session reaches the app's own Models, fields and
|
|
115
|
+
items through `node(id:)`, and the data-engine mutations. Everything else in the workspace comes
|
|
116
|
+
back as an error. Keep your data there rather than in a database of your own: it stays
|
|
117
|
+
searchable and permissioned in uidu.
|
|
118
|
+
`listModelItems` reads uidu's **search index**, which catches up a moment after a write:
|
|
119
|
+
after `createModelItem` / `deleteModelItem`, update your list from the mutation payload
|
|
120
|
+
instead of re-listing, or the new item is missing. And load models once per app instance
|
|
121
|
+
(memoize the promise): two concurrent `ensureModel` calls each create the model.
|
|
122
|
+
5. **No cookies, no `localStorage` for shared state.** In a cross-origin iframe cookies are
|
|
123
|
+
third-party (Safari drops them). `localStorage` is this browser only. Use it only as the
|
|
124
|
+
fallback when the page is opened outside uidu (the bridge rejects `connect()` there).
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
'use client'; // app/layout.tsx renders <AppProvider> around {children}
|
|
128
|
+
import { useEffect, useState } from 'react';
|
|
129
|
+
import { UiduAppProvider, useUiduApp } from '@uidu/react';
|
|
130
|
+
import { DEFAULT_HOST_ORIGINS } from '@uidu/app-bridge';
|
|
131
|
+
import {
|
|
132
|
+
createModelItem, ensureModel, listModelItems, toFieldValuesAttributes,
|
|
133
|
+
type ModelItem, type UiduClient,
|
|
134
|
+
} from '@uidu/client';
|
|
135
|
+
|
|
136
|
+
export function AppProvider({ children }: { children: React.ReactNode }) {
|
|
137
|
+
// https://*.uidu.org by default; add a local uidu or a custom domain
|
|
138
|
+
return <UiduAppProvider hostOrigins={[...DEFAULT_HOST_ORIGINS]}>{children}</UiduAppProvider>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function loadBookings(client: UiduClient, workspaceAppId: string) {
|
|
142
|
+
// interim: the app creates its own model on first load (install-time manifest comes later)
|
|
143
|
+
const model = await ensureModel(client, {
|
|
144
|
+
workspaceAppId,
|
|
145
|
+
name: 'Booking',
|
|
146
|
+
fields: [{ shortname: 'room', name: 'Room', kind: 'string' }],
|
|
147
|
+
});
|
|
148
|
+
await createModelItem(client, {
|
|
149
|
+
input: { attributes: { modelId: model.id, fieldValuesAttributes: toFieldValuesAttributes(model, { room: 'Blu' }) } },
|
|
150
|
+
});
|
|
151
|
+
return listModelItems(client, { modelId: model.id }); // item.fieldValuesByShortname.room
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function Bookings() {
|
|
155
|
+
const app = useUiduApp(); // 'connecting' | 'ready' | 'error'
|
|
156
|
+
const [items, setItems] = useState<ModelItem[]>([]);
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (app.status === 'ready') loadBookings(app.client, app.context.workspaceApp.id).then(setItems);
|
|
160
|
+
}, [app]);
|
|
161
|
+
|
|
162
|
+
if (app.status === 'error') return <p>Open this app from uidu ({app.error.code})</p>; // NOT_EMBEDDED, TIMEOUT…
|
|
163
|
+
return <ul>{items.map((item) => <li key={item.id}>{item.fieldValuesByShortname?.room}</li>)}</ul>;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`app.context` carries `user`, `space`, `workspaceApp`, `locale` and `theme`. Outside React: `connect()` from
|
|
168
|
+
`@uidu/app-bridge`, then `createClient(fromBridge(bridge))` from `@uidu/client`. Start a new
|
|
169
|
+
one with `npm create uidu-app@latest my-app -- -t custom-app`: it declares its Models in
|
|
170
|
+
`public/uidu.app.json` and sets `frame-ancestors` so only uidu can frame it.
|
|
171
|
+
|
|
91
172
|
## Working transparently
|
|
92
173
|
|
|
93
174
|
The user often can't tell what you're doing when you drive the CLI. Stay legible:
|
|
@@ -280,29 +361,103 @@ const channel = await getChannel(uidu, { id: channelId });
|
|
|
280
361
|
|
|
281
362
|
### Forms
|
|
282
363
|
|
|
283
|
-
|
|
364
|
+
Fetch the schema server-side, render it with `<DynamicForm>`, and submit through a
|
|
365
|
+
**server-side** action that calls `createFormResponse`. There is no `useForm` hook.
|
|
284
366
|
|
|
285
367
|
```tsx
|
|
286
|
-
// Server: fetch form schema
|
|
368
|
+
// Server: fetch the form schema
|
|
287
369
|
import { getForm } from '@uidu/client';
|
|
288
370
|
const form = await getForm(uidu, { id: formId });
|
|
289
371
|
```
|
|
290
372
|
|
|
291
373
|
```tsx
|
|
292
|
-
// Client: render
|
|
374
|
+
// Client: render it
|
|
293
375
|
'use client';
|
|
294
|
-
import {
|
|
376
|
+
import { DynamicForm } from '@uidu/react';
|
|
377
|
+
import { submitForm } from '@/lib/actions';
|
|
295
378
|
|
|
296
379
|
export function ContactForm({ form }) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
380
|
+
return <DynamicForm form={form} action={submitForm} />;
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
`<DynamicForm>` renders the questions itself and hands your `action` a
|
|
385
|
+
`DynamicFormValues`: `{ contact?: { firstName, lastName, email }, fieldValues:
|
|
386
|
+
Array<{ fieldId, questionId, value }>, formData }`. Return `{ ok: true }` or
|
|
387
|
+
`{ ok: false, errors }` and it renders the right state.
|
|
388
|
+
|
|
389
|
+
#### `content` is always `{ value: … }` — never a bare value
|
|
390
|
+
|
|
391
|
+
This is the one rule that breaks form writes. The API stores each answer in a JSON
|
|
392
|
+
`content` column and reads it back as `content['value']`. A flat scalar is **not**
|
|
393
|
+
rejected loudly: the response saves with empty typed columns, and reading that field
|
|
394
|
+
later throws. `FieldValueAttributes` exposes no `value` key — `content` is the only way in.
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
// ✅ Correct — every answer wrapped
|
|
398
|
+
fieldValuesAttributes: [{ fieldId: 'f1', content: { value: 'hello@example.com' } }]
|
|
399
|
+
|
|
400
|
+
// ❌ Wrong — silently loses the answer
|
|
401
|
+
fieldValuesAttributes: [{ fieldId: 'f1', content: 'hello@example.com' }]
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
It holds for **every** field kind, not just strings — `{ value: 42 }`, `{ value: true }`,
|
|
405
|
+
`{ value: '2026-07-27' }`, `{ value: ['gid://…', 'gid://…'] }` for `multipleSelect`,
|
|
406
|
+
`{ value: <doc> }` for `richText`/`json`. And it holds for `fieldValuesAttributes`
|
|
407
|
+
**wherever** it appears — on a page block, a contact, a story, a booking — not just on
|
|
408
|
+
form responses.
|
|
409
|
+
|
|
410
|
+
Full submit action:
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
// src/lib/actions.ts
|
|
414
|
+
'use server';
|
|
415
|
+
import { createFormResponse } from '@uidu/client';
|
|
416
|
+
import type { DynamicFormValues, DynamicFormResult } from '@uidu/react';
|
|
417
|
+
import { uidu } from '@/lib/uidu';
|
|
418
|
+
|
|
419
|
+
export async function submitForm(
|
|
420
|
+
values: DynamicFormValues,
|
|
421
|
+
): Promise<DynamicFormResult> {
|
|
422
|
+
const result = await createFormResponse(uidu, {
|
|
423
|
+
input: {
|
|
424
|
+
event: 'complete!', // omit to save as a draft
|
|
425
|
+
attributes: {
|
|
426
|
+
formId: FORM_ID,
|
|
427
|
+
fieldValuesAttributes: values.fieldValues.map((fv) => ({
|
|
428
|
+
fieldId: fv.fieldId,
|
|
429
|
+
content: { value: fv.value }, // ← wrapped
|
|
430
|
+
})),
|
|
431
|
+
contactAttributes: values.contact && {
|
|
432
|
+
email: values.contact.email,
|
|
433
|
+
contactableAttributes: {
|
|
434
|
+
kind: 'person',
|
|
435
|
+
firstName: values.contact.firstName,
|
|
436
|
+
lastName: values.contact.lastName,
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
if (result?.errors?.length) return { ok: false, errors: result.errors };
|
|
444
|
+
return { ok: true, meta: { id: result?.formResponse?.id } };
|
|
303
445
|
}
|
|
304
446
|
```
|
|
305
447
|
|
|
448
|
+
Notes:
|
|
449
|
+
|
|
450
|
+
- **Never put arbitrary keys in `attributes`.** Only what `FormResponseAttributes`
|
|
451
|
+
declares: `formId`, `fieldValuesAttributes`, `contactAttributes`, `contactId`,
|
|
452
|
+
`formPageId`, `completedAt`, `responsableId`, `id`. Unknown keys fail the mutation.
|
|
453
|
+
- `event` lives on `input`, **not** inside `attributes`, and is a state-machine
|
|
454
|
+
transition (`'complete!'`, `'save_draft!'`) — not free text.
|
|
455
|
+
- To record a timestamp, use `completedAt` on `attributes`, not a synthetic field value.
|
|
456
|
+
- `createFormResponse` **returns** `{ errors, formResponse }` instead of throwing —
|
|
457
|
+
always check `errors` before treating a submit as successful.
|
|
458
|
+
- Use `updateFormResponse` for multi-step or save-as-you-type flows; pass each
|
|
459
|
+
answer's existing `id` in `fieldValuesAttributes` to update it rather than add a new one.
|
|
460
|
+
|
|
306
461
|
## Rendering WYSIWYG Content
|
|
307
462
|
|
|
308
463
|
All `body` fields on Event, Story, DonationCampaign, etc. are structured documents (not strings). Render them with `<RichText>` from `@uidu/react`:
|
|
@@ -317,7 +472,7 @@ The prop is `doc`, not `children`. The component handles paragraphs, headings, l
|
|
|
317
472
|
|
|
318
473
|
## Provider Setup (Client-Side Hooks)
|
|
319
474
|
|
|
320
|
-
Server-side `await getX(...)` calls do not need a provider. For React client-side hooks (`useQuery`, `
|
|
475
|
+
Server-side `await getX(...)` calls do not need a provider. For React client-side hooks (`useQuery`, `useUiduClient`), wrap your app in `<UiduProvider>`. (`useFields` and `toText` are pure functions — they work without a provider, on the server too.)
|
|
321
476
|
|
|
322
477
|
```tsx
|
|
323
478
|
// app/layout.tsx
|
|
@@ -368,13 +523,12 @@ npm create uidu-app@latest my-app -- -t events
|
|
|
368
523
|
| `events` | Event listing + `/event/[id]` detail |
|
|
369
524
|
| `stories` | Blog listing + `/story/[id]` detail (magazine layout) |
|
|
370
525
|
| `donations` | Campaigns listing + `/campaign/[id]` detail with progress bars |
|
|
526
|
+
| `custom-app` | An app inside a uidu Space: `<UiduAppProvider>`, data in its own Models (`public/uidu.app.json`) |
|
|
371
527
|
|
|
372
|
-
Every template ships with
|
|
373
|
-
|
|
374
|
-
-
|
|
375
|
-
|
|
376
|
-
- A server-rendered listing + dynamic detail page
|
|
377
|
-
- `.env.example` with required vars (and the CLI offers to fill them in for you)
|
|
528
|
+
Every template ships with Next.js 16 App Router, Tailwind v4 and an `.env.example` (the CLI
|
|
529
|
+
offers to fill it in for you). The site templates add a configured `src/lib/uidu.ts` and a
|
|
530
|
+
server-rendered listing + dynamic detail page; `custom-app` has no token to configure and
|
|
531
|
+
reads everything in client components.
|
|
378
532
|
|
|
379
533
|
## See Also
|
|
380
534
|
|