@comet/agent-features 9.0.0-canary-20260527154746
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/LICENSE +24 -0
- package/package.json +19 -0
- package/rules/coding-guidelines/api-nestjs.instructions.md +107 -0
- package/rules/coding-guidelines/cdn.instructions.md +24 -0
- package/rules/coding-guidelines/general.instructions.md +30 -0
- package/rules/coding-guidelines/git.instructions.md +37 -0
- package/rules/coding-guidelines/kubernetes.instructions.md +59 -0
- package/rules/coding-guidelines/libraries.instructions.md +34 -0
- package/rules/coding-guidelines/naming.instructions.md +39 -0
- package/rules/coding-guidelines/postgresql.instructions.md +40 -0
- package/rules/coding-guidelines/react.instructions.md +102 -0
- package/rules/coding-guidelines/security.instructions.md +44 -0
- package/rules/coding-guidelines/styling.instructions.md +50 -0
- package/rules/coding-guidelines/typescript.instructions.md +50 -0
- package/skills/.gitkeep +0 -0
- package/skills/comet-block/SKILL.md +246 -0
- package/skills/comet-block/references/admin-patterns.md +192 -0
- package/skills/comet-block/references/api-patterns.md +183 -0
- package/skills/comet-block/references/block-loader.md +368 -0
- package/skills/comet-block/references/block-types.md +210 -0
- package/skills/comet-block/references/custom-block-field.md +266 -0
- package/skills/comet-block/references/fixtures.md +436 -0
- package/skills/comet-block/references/image.md +341 -0
- package/skills/comet-block/references/migration.md +597 -0
- package/skills/comet-block/references/registration.md +167 -0
- package/skills/comet-block/references/response-summary.md +102 -0
- package/skills/comet-block/references/rich-text.md +309 -0
- package/skills/comet-block/references/select.md +176 -0
- package/skills/comet-block/references/site-patterns.md +202 -0
- package/skills/comet-mail-react/SKILL.md +539 -0
- package/skills/comet-mail-react/references/components-and-theme.md +395 -0
- package/skills/comet-mail-react/references/layout-patterns.md +315 -0
- package/skills/comet-mail-react/references/styling-and-customization.md +306 -0
- package/skills/comet-minor-update/SKILL.md +191 -0
- package/skills/dev-pm/SKILL.md +100 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: TypeScript style — options objects, named exports, async/await, enums
|
|
3
|
+
applyTo: "**/*.ts,**/*.tsx"
|
|
4
|
+
paths:
|
|
5
|
+
- "**/*.{ts,tsx}"
|
|
6
|
+
globs:
|
|
7
|
+
- "**/*.{ts,tsx}"
|
|
8
|
+
alwaysApply: false
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# TypeScript Rules
|
|
12
|
+
|
|
13
|
+
## Function signatures
|
|
14
|
+
|
|
15
|
+
- If a function takes **more than 2** parameters, use a single options object. This makes call sites self-documenting and avoids argument-order mistakes.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// Bad
|
|
19
|
+
getSortedJobs("createdAt", true, 20);
|
|
20
|
+
|
|
21
|
+
// Good
|
|
22
|
+
getSortedJobs({ orderBy: "createdAt", includeSoftDeleted: true, limit: 20 });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- Use default argument values (`function f(name = "X")`) instead of `name || "X"` or conditional fallbacks inside the body.
|
|
26
|
+
- Prefer `param?: T` over `param: T | undefined` — the optional form does not require callers to explicitly pass `undefined`.
|
|
27
|
+
|
|
28
|
+
## Imports / exports
|
|
29
|
+
|
|
30
|
+
- Use **named exports only**. Default exports are allowed only when technically required (e.g. a framework demands it).
|
|
31
|
+
- Use relative imports only for **sibling or child** files within the same module. Everything else uses the `@src/…` alias.
|
|
32
|
+
|
|
33
|
+
## Async & iteration
|
|
34
|
+
|
|
35
|
+
- Prefer `async/await` over raw callbacks or `.then()` chains.
|
|
36
|
+
- Prefer `for…of` over `Array.prototype.forEach` — it supports `await`, `break`, `continue`, and all iterables.
|
|
37
|
+
|
|
38
|
+
## Enums
|
|
39
|
+
|
|
40
|
+
- Enum **keys** and **values** use `camelCase`, and keys must equal their values.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
enum Direction {
|
|
44
|
+
north = "north",
|
|
45
|
+
northEast = "northEast",
|
|
46
|
+
// …
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- GraphQL-exposed enums follow a different rule — see [api-nestjs.instructions.md](api-nestjs.instructions.md#graphql-enums).
|
package/skills/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: comet-block
|
|
3
|
+
description: Creates and edits Comet blocks (API, Admin, Site) from natural-language prompts, including block fixture services. Use when the user asks to create a new block, edit an existing block, add/remove/change fields or child blocks, scaffold block files, or create block fixtures in a Comet project.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Comet Block Skill
|
|
7
|
+
|
|
8
|
+
## Table of Contents
|
|
9
|
+
|
|
10
|
+
1. [When to use](#when-to-use)
|
|
11
|
+
2. [Workflow routing](#workflow-routing)
|
|
12
|
+
3. [Step 1 — Parse the prompt](#step-1--parse-the-prompt)
|
|
13
|
+
4. [Step 2 — Discover the project](#step-2--discover-the-project)
|
|
14
|
+
5. [Editing workflow](#editing-workflow) ← follow this for edits
|
|
15
|
+
6. [Step 3 — Create the API block](#step-3--create-the-api-block) ← creation only
|
|
16
|
+
7. [Step 4 — Create the Admin block](#step-4--create-the-admin-block) ← creation only
|
|
17
|
+
8. [Step 5 — Create the Site block](#step-5--create-the-site-block) ← creation only
|
|
18
|
+
9. [Step 6 — Register the block](#step-6--register-the-block)
|
|
19
|
+
10. [Step 7 — Create block fixtures](#step-7--create-block-fixtures)
|
|
20
|
+
11. [Naming conventions](#naming-conventions)
|
|
21
|
+
12. [Code generation](#code-generation)
|
|
22
|
+
13. [Cross-references](#cross-references)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## When to use
|
|
27
|
+
|
|
28
|
+
- Creating a **new** Comet block from a natural-language description.
|
|
29
|
+
- Scaffolding block files across API, Admin, and Site layers.
|
|
30
|
+
- Adding, removing, or changing fields or child blocks in an existing block.
|
|
31
|
+
- Changing enum values, field types, or property names in an existing block.
|
|
32
|
+
- Creating block fixture services for seeding development databases.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Workflow routing
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
User request
|
|
40
|
+
│
|
|
41
|
+
├─ Creating a new block? → Steps 1 → 2 → 3 → 4 → 5 → 6 → 7
|
|
42
|
+
│
|
|
43
|
+
└─ Editing an existing block? → Steps 1 → 2 → Editing workflow → 7
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Step 1 — Parse the prompt
|
|
49
|
+
|
|
50
|
+
Extract from the user's request:
|
|
51
|
+
|
|
52
|
+
1. **Block name** — derive PascalCase for Admin/Site (`TeaserItemBlock`) and kebab-case for API (`teaser-item.block.ts`). See [Naming conventions](#naming-conventions).
|
|
53
|
+
2. **Properties** — for each property determine its type. Common types:
|
|
54
|
+
- **String** — plain text (`title`, `label`).
|
|
55
|
+
- **Number** — numeric with min/max (`overlay`).
|
|
56
|
+
- **Boolean** — toggle/switch (`showOverlay`).
|
|
57
|
+
- **Numeric select** — fixed numeric options. Use `@IsInt()` + `@BlockField()` in the API (not `type: "enum"`); use `createCompositeBlockSelectField` with number options in Admin. See [select.md](references/select.md).
|
|
58
|
+
- **Enum/select** — fixed string values (`variant`, `alignment`). See [select.md](references/select.md).
|
|
59
|
+
- **RichText** — formatted text. Choose shared `RichTextBlock` or a scoped inline one. See [rich-text.md](references/rich-text.md).
|
|
60
|
+
- **Image** — choose `DamImageBlock`, `PixelImageBlock`, `SvgImageBlock`, or a project-specific `MediaBlock`. See [image.md](references/image.md).
|
|
61
|
+
- **Child block** — any other existing block used as a property.
|
|
62
|
+
- **List** — multiple instances of a child block; requires a list block + item block pair. See [block-types.md](references/block-types.md).
|
|
63
|
+
3. **Registration target** — where the block is added. Default: `PageContentBlock` (and `ContentGroupBlock` if it exists). Ask if unclear.
|
|
64
|
+
4. **Ambiguities** — if "image" is mentioned without specifics, or a referenced block may not exist, ask before proceeding.
|
|
65
|
+
|
|
66
|
+
For block type decision guidance, see [block-types.md](references/block-types.md).
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Step 2 — Discover the project
|
|
71
|
+
|
|
72
|
+
Before generating any code:
|
|
73
|
+
|
|
74
|
+
1. **Locate `api`, `admin`, `site` directories.** The `site` directory is optional.
|
|
75
|
+
2. **Find existing blocks directories** — typically `src/documents/pages/blocks/`. Some shared blocks live in `common/blocks/` or similar. Check both.
|
|
76
|
+
3. **Verify referenced blocks exist** — search for any blocks named in the prompt (e.g., `HeadingBlock`, `LinkBlock`) in all layers and note their import paths.
|
|
77
|
+
4. **Find registration targets** — search for `createBlocksBlock` usages to locate `PageContentBlock`, `ContentGroupBlock`, or other targets. Note file paths in all layers.
|
|
78
|
+
5. **Confirm site directory** — if absent, skip all site steps.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Editing workflow
|
|
83
|
+
|
|
84
|
+
Use this instead of Steps 3–5 when modifying an existing block.
|
|
85
|
+
|
|
86
|
+
### Classify each change
|
|
87
|
+
|
|
88
|
+
| Change type | Description |
|
|
89
|
+
| ---------------------------- | ---------------------------------------------------- |
|
|
90
|
+
| **Add field/child block** | A new property on the block. |
|
|
91
|
+
| **Remove field/child block** | An existing property is deleted. |
|
|
92
|
+
| **Change field type** | One type replaces another (e.g., string → RichText). |
|
|
93
|
+
| **Change enum values** | Options added to or removed from a select field. |
|
|
94
|
+
| **Rename field** | Property keeps its type but gets a new name. |
|
|
95
|
+
|
|
96
|
+
A single request may involve multiple change types — classify each independently.
|
|
97
|
+
|
|
98
|
+
### Determine if a migration is needed
|
|
99
|
+
|
|
100
|
+
Ask the user before creating a migration if they haven't mentioned it. Explain the old vs new data shape and confirm.
|
|
101
|
+
|
|
102
|
+
**Migration IS needed:**
|
|
103
|
+
|
|
104
|
+
- Adding a **required** field (existing data lacks it).
|
|
105
|
+
- **Changing** a field's type (old data has the old shape).
|
|
106
|
+
- **Removing** a field (recommended to keep data clean).
|
|
107
|
+
- **Renaming** a field (old data uses the old name).
|
|
108
|
+
|
|
109
|
+
**Migration is NOT needed:**
|
|
110
|
+
|
|
111
|
+
- Adding an **optional/nullable** field (`@IsUndefinable()` + `@BlockField({ nullable: true })`).
|
|
112
|
+
- Adding a new supported block to a `BlocksBlock` or `OneOfBlock`.
|
|
113
|
+
- Adding new enum values (existing data unaffected).
|
|
114
|
+
|
|
115
|
+
When in doubt, create a migration — it is always the safer choice. See [migration.md](references/migration.md) for the full decision matrix, class template, and annotated examples.
|
|
116
|
+
|
|
117
|
+
### Apply changes in order
|
|
118
|
+
|
|
119
|
+
1. **API block** — update `BlockData` and `BlockInput` classes, decorators, validators.
|
|
120
|
+
2. **Migration** — create and register if needed. Update `createBlock` third argument to the options object. See [migration.md](references/migration.md).
|
|
121
|
+
3. **Admin block** — update the `blocks` object: add/remove entries, labels, options.
|
|
122
|
+
4. **Site block** (if exists) — update destructured fields and rendered output.
|
|
123
|
+
5. **Block fixture** (if exists) — update `generateBlockInput()` to match changes. See [fixtures.md](references/fixtures.md).
|
|
124
|
+
6. **Verify consistency** — confirm every property key name matches across all three layers.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Step 3 — Create the API block
|
|
129
|
+
|
|
130
|
+
File: `{block-name}.block.ts` (kebab-case). Place in the blocks directory found in Step 2.
|
|
131
|
+
|
|
132
|
+
**Key patterns:**
|
|
133
|
+
|
|
134
|
+
- `BlockData` uses `@BlockField()` for fields, `@ChildBlock(X)` for child blocks.
|
|
135
|
+
- `BlockInput` uses validators + `@ChildBlockInput(X)` for child blocks; implement `transformToBlockData()` with `inputToData`.
|
|
136
|
+
- Export with `createBlock(BlockData, BlockInput, "BlockName")`.
|
|
137
|
+
- Enums require `@BlockField({ type: "enum", enum: MyEnum })` — never use `type: "enum"` for numeric options.
|
|
138
|
+
- For list blocks: create the item block first, then `createListBlock({ block: ItemBlock }, "MyList")`.
|
|
139
|
+
|
|
140
|
+
For field decorators, validator reference, savability rules, and complete examples, see [api-patterns.md](references/api-patterns.md).
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Step 4 — Create the Admin block
|
|
145
|
+
|
|
146
|
+
File: `{BlockName}Block.tsx` (PascalCase). Place in the blocks directory found in Step 2.
|
|
147
|
+
|
|
148
|
+
**Key patterns:**
|
|
149
|
+
|
|
150
|
+
- Use `createCompositeBlock` with a `blocks` object mapping property names to block configs.
|
|
151
|
+
- Helper functions: `createCompositeBlockTextField`, `createCompositeBlockSelectField`, `createCompositeBlockSwitchField`.
|
|
152
|
+
- `fullWidth` defaults to `true` in text and select helpers — omit it explicitly.
|
|
153
|
+
- All user-facing strings use `FormattedMessage` from `react-intl`. Follow ID convention: `{blockName}Block.{field}`.
|
|
154
|
+
- Set `BlockCategory` when the block is used inside a blocks block.
|
|
155
|
+
- Set `previewContent` in the override callback for meaningful block list previews.
|
|
156
|
+
- **`hiddenInSubroute` rule:** When the composite contains sub-route blocks (list, blocks-block, one-of), set `hiddenInSubroute: true` on every sibling entry that is _not_ a sub-route block. Never set it on sub-route block entries themselves.
|
|
157
|
+
- **`label` vs `title`:** when the helper has a `label`, omit `title` on the block entry. When it has no `label`, provide `title` on the entry. They are mutually exclusive.
|
|
158
|
+
- For list blocks: use `createListBlock` from `@comet/cms-admin` with `name`, `displayName`, `block`, `itemName`, `itemsName`.
|
|
159
|
+
|
|
160
|
+
For the full helper API, `BlockCategory` enum, and complete examples, see [admin-patterns.md](references/admin-patterns.md).
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Step 5 — Create the Site block (if site exists)
|
|
165
|
+
|
|
166
|
+
File: `{BlockName}Block.tsx` (PascalCase). Place in the blocks directory found in Step 2.
|
|
167
|
+
|
|
168
|
+
**Key patterns:**
|
|
169
|
+
|
|
170
|
+
- Wrap with `withPreview(Component, { label: "BlockName" })`.
|
|
171
|
+
- Type props as `PropsWithData<{BlockName}BlockData>` — import from `@src/blocks.generated`.
|
|
172
|
+
- Use `hasRichTextBlockContent` guard before rendering RichText. Never wrap `RichTextBlock` in a `Typography` component.
|
|
173
|
+
- `DamImageBlock` is **not** exported from `@comet/site-nextjs` — use the project-specific wrapper. See [image.md](references/image.md).
|
|
174
|
+
- Always validate `aspectRatio` against `allowedImageAspectRatios` in the API config. See [image.md](references/image.md).
|
|
175
|
+
- The `supportedBlocks` object (for `BlocksBlock`/`PageContent` site wrapper) must be defined at module level, not inside the component.
|
|
176
|
+
|
|
177
|
+
For `withPreview`, `OneOfBlock`/`OptionalBlock`, and complete site examples, see [site-patterns.md](references/site-patterns.md).
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Step 6 — Register the block
|
|
182
|
+
|
|
183
|
+
Add the new block to the registration target in **all three layers** using **identical camelCase key names**.
|
|
184
|
+
|
|
185
|
+
**Default target:** `PageContentBlock`. If `ContentGroupBlock` exists and mirrors `PageContentBlock` supported blocks, also register there.
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
// API & Admin: supportedBlocks object inside createBlocksBlock
|
|
189
|
+
myBlock: MyBlock,
|
|
190
|
+
|
|
191
|
+
// Site: supportedBlocks object
|
|
192
|
+
myBlock: (props) => <MyBlock data={props} />,
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
For the target hierarchy, multiple targets, and edge cases, see [registration.md](references/registration.md).
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Step 7 — Create block fixtures
|
|
200
|
+
|
|
201
|
+
Create a fixture service that generates realistic seed data for the new block.
|
|
202
|
+
|
|
203
|
+
1. **Create the fixture service** matching the block type (composite, list, blocks-block, one-of).
|
|
204
|
+
2. **Wire into the parent block's fixture.** If the parent has no fixture, ask the user before creating one.
|
|
205
|
+
3. **Register in `fixtures.module.ts`** as a provider.
|
|
206
|
+
4. **For nested list blocks:** create the list item fixture service first, then the parent composite fixture which injects and calls it.
|
|
207
|
+
|
|
208
|
+
For faker guidelines, patterns by block type, and full examples, see [fixtures.md](references/fixtures.md).
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Naming conventions
|
|
213
|
+
|
|
214
|
+
| Concept | Convention | Example |
|
|
215
|
+
| -------------------------------- | ------------------------- | --------------------------- |
|
|
216
|
+
| API file name | kebab-case `.block.ts` | `product-card.block.ts` |
|
|
217
|
+
| Admin / Site file name | PascalCase `Block.tsx` | `ProductCardBlock.tsx` |
|
|
218
|
+
| Export variable | PascalCase + "Block" | `ProductCardBlock` |
|
|
219
|
+
| Name string (`createBlock` etc.) | PascalCase, no "Block" | `"ProductCard"` |
|
|
220
|
+
| API `BlockData` class | PascalCase + "BlockData" | `ProductCardBlockData` |
|
|
221
|
+
| API `BlockInput` class | PascalCase + "BlockInput" | `ProductCardBlockInput` |
|
|
222
|
+
| Registration key | camelCase, no "Block" | `productCard` |
|
|
223
|
+
| Child block property | camelCase | `image`, `callToActionList` |
|
|
224
|
+
| `FormattedMessage` ID | camelCase block + field | `productCardBlock.headline` |
|
|
225
|
+
|
|
226
|
+
The `name` parameter in `createBlock`, `createCompositeBlock`, `createListBlock`, `createBlocksBlock`, and `createOneOfBlock` is PascalCase **without** a "Block" suffix and must be **identical** across all layers.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Cross-references
|
|
231
|
+
|
|
232
|
+
| Topic | File |
|
|
233
|
+
| --------------------------------------------------- | --------------------------------------------------------- |
|
|
234
|
+
| Block type overview and decision guide | [block-types.md](references/block-types.md) |
|
|
235
|
+
| API decorator patterns, validators, savability | [api-patterns.md](references/api-patterns.md) |
|
|
236
|
+
| Admin helpers, `BlockCategory`, `hiddenInSubroute` | [admin-patterns.md](references/admin-patterns.md) |
|
|
237
|
+
| Site `withPreview`, rendering patterns | [site-patterns.md](references/site-patterns.md) |
|
|
238
|
+
| Registration targets, keys, edge cases | [registration.md](references/registration.md) |
|
|
239
|
+
| RichText configuration, scoped vs shared | [rich-text.md](references/rich-text.md) |
|
|
240
|
+
| Enum/select patterns, numeric options, multi-select | [select.md](references/select.md) |
|
|
241
|
+
| Image block selection and site wrappers | [image.md](references/image.md) |
|
|
242
|
+
| Migration decision matrix, class template, examples | [migration.md](references/migration.md) |
|
|
243
|
+
| Block fixture patterns and faker guidelines | [fixtures.md](references/fixtures.md) |
|
|
244
|
+
| Block loaders for server-side data fetching | [block-loader.md](references/block-loader.md) |
|
|
245
|
+
| Custom block fields for entity selection | [custom-block-field.md](references/custom-block-field.md) |
|
|
246
|
+
| Response template for creation/editing output | [response-summary.md](references/response-summary.md) |
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Admin Block Patterns
|
|
2
|
+
|
|
3
|
+
Admin blocks live in `admin/src/`, typically under `documents/pages/blocks/` or `common/blocks/`. File names use PascalCase: `{BlockName}Block.tsx`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Imports
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import {
|
|
11
|
+
BlockCategory,
|
|
12
|
+
createCompositeBlock,
|
|
13
|
+
createCompositeBlockSelectField,
|
|
14
|
+
createCompositeBlockSwitchField,
|
|
15
|
+
createCompositeBlockTextField,
|
|
16
|
+
} from "@comet/cms-admin";
|
|
17
|
+
import { type MyBlockData } from "@src/blocks.generated";
|
|
18
|
+
import { FormattedMessage } from "react-intl";
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Image blocks (`DamImageBlock`, `PixelImageBlock`, `SvgImageBlock`) are imported from `@comet/cms-admin`.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## createCompositeBlock
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
export const MyBlock = createCompositeBlock(
|
|
29
|
+
{
|
|
30
|
+
name: "My", // PascalCase, no "Block" suffix; must match API
|
|
31
|
+
displayName: <FormattedMessage id="myBlock.displayName" defaultMessage="My" />,
|
|
32
|
+
category: BlockCategory.TextAndContent, // only when used inside a blocks block
|
|
33
|
+
blocks: {
|
|
34
|
+
/* ... */
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
(block) => {
|
|
38
|
+
block.previewContent = (state) => [{ type: "text", content: state.title }];
|
|
39
|
+
return block;
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The second argument (override function) is optional. Always return the block from it.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Block Configuration Object Keys
|
|
49
|
+
|
|
50
|
+
Each key in `blocks` maps to a property from the API. The value is a configuration object with:
|
|
51
|
+
|
|
52
|
+
- `block` — the block instance or field helper (required)
|
|
53
|
+
- `title` — section heading above the block; use `FormattedMessage`
|
|
54
|
+
- `hiddenInSubroute` — hide when user navigates into a sub-route (see below)
|
|
55
|
+
- `nested` — render as a nested page with a navigation button instead of inline
|
|
56
|
+
- `paper` — wrap in a card container
|
|
57
|
+
- `divider` — show a divider below (only inside a paper container)
|
|
58
|
+
- `hiddenForState` — `(state) => boolean`; conditionally hide based on block state
|
|
59
|
+
|
|
60
|
+
### `label` vs `title` — mutually exclusive
|
|
61
|
+
|
|
62
|
+
- Field helpers (`createCompositeBlockTextField`, `createCompositeBlockSelectField`, `createCompositeBlockSwitchField`) accept a `label` prop that renders inline with the field.
|
|
63
|
+
- The block configuration entry has a `title` prop that renders as a section heading above the block.
|
|
64
|
+
- **Never set both.** When the helper has a `label`, omit `title` on the entry. When the helper has no `label`, provide `title` on the entry.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// Correct: label on the helper, no title on the entry
|
|
68
|
+
title: {
|
|
69
|
+
block: createCompositeBlockTextField({
|
|
70
|
+
label: <FormattedMessage id="myBlock.title" defaultMessage="Title" />,
|
|
71
|
+
}),
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Correct: no label on the helper, title on the entry
|
|
75
|
+
image: {
|
|
76
|
+
block: DamImageBlock,
|
|
77
|
+
title: <FormattedMessage id="myBlock.image" defaultMessage="Image" />,
|
|
78
|
+
},
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### `fullWidth` default
|
|
82
|
+
|
|
83
|
+
`createCompositeBlockTextField`, `createCompositeBlockSelectField`, and `createCompositeBlockSwitchField` all default `fullWidth` to `true`. Do not pass it explicitly unless overriding to `false`.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## hiddenInSubroute Rules
|
|
88
|
+
|
|
89
|
+
When a composite block contains sub-route children (list blocks, blocks blocks, one-of blocks), the admin renders them in a nested route. Sibling entries that are not sub-route blocks must be hidden while the user is inside the sub-route, otherwise the layout breaks.
|
|
90
|
+
|
|
91
|
+
**Rule:** Set `hiddenInSubroute: true` on every entry that is **not** itself a sub-route block. **Never set it on list/blocks/one-of block entries** — they must stay visible to show their sub-route content.
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
blocks: {
|
|
95
|
+
// Non-sub-route entries: hide in sub-route
|
|
96
|
+
title: {
|
|
97
|
+
block: createCompositeBlockTextField({
|
|
98
|
+
label: <FormattedMessage id="myBlock.title" defaultMessage="Title" />,
|
|
99
|
+
}),
|
|
100
|
+
hiddenInSubroute: true,
|
|
101
|
+
},
|
|
102
|
+
variant: {
|
|
103
|
+
block: createCompositeBlockSelectField<MyBlockData["variant"]>({
|
|
104
|
+
defaultValue: "primary",
|
|
105
|
+
options: variantOptions,
|
|
106
|
+
label: <FormattedMessage id="myBlock.variant" defaultMessage="Variant" />,
|
|
107
|
+
}),
|
|
108
|
+
hiddenInSubroute: true,
|
|
109
|
+
},
|
|
110
|
+
// Sub-route entry: do NOT set hiddenInSubroute
|
|
111
|
+
items: {
|
|
112
|
+
block: MyItemsListBlock,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
If the composite has **no** sub-route children, omit `hiddenInSubroute` entirely.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## BlockCategory Enum
|
|
122
|
+
|
|
123
|
+
Import from `@comet/cms-admin`. Only set when the block is used inside a blocks block.
|
|
124
|
+
|
|
125
|
+
| Value | Typical use |
|
|
126
|
+
| ------------------- | ------------------------------------- |
|
|
127
|
+
| `TextAndContent` | Text, headings, rich text, accordions |
|
|
128
|
+
| `Media` | Images, videos, galleries |
|
|
129
|
+
| `Navigation` | Links, CTAs, anchors |
|
|
130
|
+
| `Teaser` | Teasers, stage blocks |
|
|
131
|
+
| `StructuredContent` | Data-driven / structured content |
|
|
132
|
+
| `Layout` | Columns, spacers, layout wrappers |
|
|
133
|
+
| `Form` | Form blocks |
|
|
134
|
+
| `Other` | Default when no category fits |
|
|
135
|
+
|
|
136
|
+
Set via the `category` option or in the override function:
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
(block) => {
|
|
140
|
+
block.category = BlockCategory.Media;
|
|
141
|
+
return block;
|
|
142
|
+
};
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## previewContent Callback
|
|
148
|
+
|
|
149
|
+
Controls the collapsed preview row of a block inside list/blocks blocks. Set in the override function.
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
// Show a string field
|
|
153
|
+
block.previewContent = (state) => [{ type: "text", content: state.title }];
|
|
154
|
+
|
|
155
|
+
// Handle optional fields
|
|
156
|
+
block.previewContent = (state) => (state.title !== undefined ? [{ type: "text", content: state.title }] : []);
|
|
157
|
+
|
|
158
|
+
// Nothing to show
|
|
159
|
+
block.previewContent = () => [];
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Always set `previewContent` for blocks shown inside list or blocks blocks. Always guard optional fields to avoid returning undefined as content.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## FormattedMessage ID Conventions
|
|
167
|
+
|
|
168
|
+
All user-facing strings use `FormattedMessage` from `react-intl`. Always provide `defaultMessage`.
|
|
169
|
+
|
|
170
|
+
| Context | ID pattern | Example |
|
|
171
|
+
| --------------------- | -------------------------------------- | --------------------------------- |
|
|
172
|
+
| Block display name | `{blockName}Block.displayName` | `featureItemBlock.displayName` |
|
|
173
|
+
| Field label / title | `{blockName}Block.{fieldName}` | `featureItemBlock.title` |
|
|
174
|
+
| Select option label | `{blockName}Block.{fieldName}.{value}` | `featureItemBlock.alignment.left` |
|
|
175
|
+
| List block item name | `{blockName}Block.itemName` | `featureListBlock.itemName` |
|
|
176
|
+
| List block items name | `{blockName}Block.itemsName` | `featureListBlock.itemsName` |
|
|
177
|
+
|
|
178
|
+
IDs use camelCase dot-separated segments. The block name segment uses the full block name with "Block" suffix (`featureItemBlock`, not `featureItem`).
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Naming Conventions
|
|
183
|
+
|
|
184
|
+
| Element | Convention | Example |
|
|
185
|
+
| ---------------------------- | -------------------------------------- | ------------------------------------ |
|
|
186
|
+
| File name | PascalCase ending in `Block.tsx` | `ProductCardBlock.tsx` |
|
|
187
|
+
| Exported constant | `{BlockName}Block` | `ProductCardBlock` |
|
|
188
|
+
| `name` option | PascalCase, **no** "Block" suffix | `"ProductCard"` |
|
|
189
|
+
| Keys in `blocks` | camelCase, matching API property names | `callToActionList`, `titleHtmlTag` |
|
|
190
|
+
| Keys in `supportedBlocks` | camelCase, matching API and Site keys | `richText`, `heading`, `featureList` |
|
|
191
|
+
| FormattedMessage IDs | camelCase dot-separated | `productCardBlock.displayName` |
|
|
192
|
+
| Locally-scoped helper blocks | Descriptive PascalCase | `DescriptionRichTextBlock` |
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# API Block Patterns
|
|
2
|
+
|
|
3
|
+
Comet-specific patterns for API-layer block definitions. All API blocks live in `{block-name}.block.ts` (kebab-case) inside `api/src/`, typically under `documents/pages/blocks/` or `common/blocks/`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Imports
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
BlockData,
|
|
12
|
+
BlockDataInterface,
|
|
13
|
+
BlockField,
|
|
14
|
+
BlockInput,
|
|
15
|
+
blockInputToData,
|
|
16
|
+
ChildBlock,
|
|
17
|
+
ChildBlockInput,
|
|
18
|
+
createBlock,
|
|
19
|
+
ExtractBlockInput,
|
|
20
|
+
IsUndefinable, // from @comet/cms-api, not class-validator
|
|
21
|
+
} from "@comet/cms-api";
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Factory functions (`createListBlock`, `createBlocksBlock`, `createOneOfBlock`, `createOptionalBlock`) are also imported from `@comet/cms-api`. Standard validators (`@IsString`, `@IsInt`, `@IsBoolean`, `@IsEnum`, `@Min`, `@Max`) come from `class-validator`.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## BlockData and BlockInput
|
|
29
|
+
|
|
30
|
+
`BlockData` declares the output shape; `BlockInput` declares what the admin sends. Neither class is exported — only the final block constant is.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
class MyBlockData extends BlockData {
|
|
34
|
+
@BlockField({ nullable: true })
|
|
35
|
+
title?: string;
|
|
36
|
+
|
|
37
|
+
@ChildBlock(DamImageBlock)
|
|
38
|
+
image: BlockDataInterface;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class MyBlockInput extends BlockInput {
|
|
42
|
+
@IsUndefinable()
|
|
43
|
+
@IsString()
|
|
44
|
+
@BlockField({ nullable: true })
|
|
45
|
+
title?: string;
|
|
46
|
+
|
|
47
|
+
@ChildBlockInput(DamImageBlock)
|
|
48
|
+
image: ExtractBlockInput<typeof DamImageBlock>;
|
|
49
|
+
|
|
50
|
+
transformToBlockData(): MyBlockData {
|
|
51
|
+
return blockInputToData(MyBlockData, this);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const MyBlock = createBlock(MyBlockData, MyBlockInput, "My");
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Key rules:
|
|
59
|
+
|
|
60
|
+
- `transformToBlockData()` must call `blockInputToData(DataClass, this)` — never manually map properties.
|
|
61
|
+
- `@ChildBlock` properties in `BlockData` are always typed as `BlockDataInterface`.
|
|
62
|
+
- `@ChildBlockInput` properties in `BlockInput` are always typed as `ExtractBlockInput<typeof SomeBlock>`.
|
|
63
|
+
- The third argument to `createBlock` is PascalCase **without** a "Block" suffix and must be unique across the project.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## @IsUndefinable vs @IsOptional
|
|
68
|
+
|
|
69
|
+
**Always use `@IsUndefinable()` from `@comet/cms-api` for nullable fields — never `@IsOptional()`.**
|
|
70
|
+
|
|
71
|
+
- `@IsUndefinable()` permits only `undefined` and enforces all other validators on non-undefined values.
|
|
72
|
+
- `@IsOptional()` also allows `null` and silently skips all other validators — this can hide validation bugs.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// Correct
|
|
76
|
+
@IsUndefinable()
|
|
77
|
+
@IsString()
|
|
78
|
+
@BlockField({ nullable: true })
|
|
79
|
+
title?: string;
|
|
80
|
+
|
|
81
|
+
// Wrong — @IsOptional() skips @IsString() when the value is absent
|
|
82
|
+
@IsOptional()
|
|
83
|
+
@IsString()
|
|
84
|
+
@BlockField({ nullable: true })
|
|
85
|
+
title?: string;
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## @BlockField options
|
|
91
|
+
|
|
92
|
+
| Option | Notes |
|
|
93
|
+
| ---------- | ---------------------------------------------------------------------------------- |
|
|
94
|
+
| `nullable` | Allow `undefined`. Always pair with `@IsUndefinable()` on the input property. |
|
|
95
|
+
| `type` | Required for `"enum"` and `"json"`. Auto-detected for string, number, and boolean. |
|
|
96
|
+
| `enum` | Required when `type: "enum"`. Pass the enum object or string array. |
|
|
97
|
+
| `array` | Mark the field as an array of the given type. |
|
|
98
|
+
|
|
99
|
+
### Enum fields — always specify `type: "enum"`
|
|
100
|
+
|
|
101
|
+
The `type: "enum"` option is **required** — omitting it causes incorrect type inference.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
export enum Alignment {
|
|
105
|
+
left = "left",
|
|
106
|
+
center = "center",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// BlockData
|
|
110
|
+
@BlockField({ type: "enum", enum: Alignment })
|
|
111
|
+
alignment: Alignment;
|
|
112
|
+
|
|
113
|
+
// BlockInput
|
|
114
|
+
@IsEnum(Alignment)
|
|
115
|
+
@BlockField({ type: "enum", enum: Alignment })
|
|
116
|
+
alignment: Alignment;
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Enum fields are never nullable — always provide a `defaultValue` in the Admin.
|
|
120
|
+
|
|
121
|
+
### Enum array (multi-select)
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
// BlockData
|
|
125
|
+
@BlockField({ type: "enum", enum: ProductType, array: true })
|
|
126
|
+
types: ProductType[];
|
|
127
|
+
|
|
128
|
+
// BlockInput
|
|
129
|
+
@IsEnum(ProductType, { each: true })
|
|
130
|
+
@BlockField({ type: "enum", enum: ProductType, array: true })
|
|
131
|
+
types: ProductType[];
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`@IsEnum(X, { each: true })` accepts empty arrays, so multi-select fields are automatically savable.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Savability
|
|
139
|
+
|
|
140
|
+
All blocks must be savable in their initial (empty) state — admin users must be able to add a block and save without filling in any content.
|
|
141
|
+
|
|
142
|
+
| Field type | API pattern | Admin default | Pitfall |
|
|
143
|
+
| ----------- | ---------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------ |
|
|
144
|
+
| String | `@IsUndefinable()` + `@IsString()` + `@BlockField({ nullable: true })` + `?` | `""` (automatic) | `@IsString()` rejects `undefined` without `@IsUndefinable()` |
|
|
145
|
+
| Enum | `@IsEnum(X)` + `@BlockField({ type: "enum", enum: X })` | Must set `defaultValue` | `@IsEnum()` rejects `undefined` |
|
|
146
|
+
| Enum array | `@IsEnum(X, { each: true })` + `{ array: true }` | `[]` (automatic) | Accepts empty arrays by default |
|
|
147
|
+
| Number | `@IsInt()` + `@Min()` + `@Max()` + `@BlockField()` | Must set `defaultValue` | `@IsInt()` rejects `undefined` |
|
|
148
|
+
| Boolean | `@IsBoolean()` + `@BlockField()` | `false` (automatic) | `@IsBoolean()` rejects `undefined`; switch sends `false` |
|
|
149
|
+
| Child block | `@ChildBlockInput(X)` typed as `ExtractBlockInput<typeof X>` | N/A | Each child block handles its own empty state |
|
|
150
|
+
|
|
151
|
+
**Strings should almost always be optional** to allow saving with no content.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Factory blocks (API)
|
|
156
|
+
|
|
157
|
+
Full block-type examples live in [block-types.md](block-types.md). The critical API-layer rules:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
// List block
|
|
161
|
+
export const FeatureListBlock = createListBlock({ block: FeatureItemBlock }, "FeatureList");
|
|
162
|
+
|
|
163
|
+
// Blocks block
|
|
164
|
+
export const PageContentBlock = createBlocksBlock({ supportedBlocks: { richText: RichTextBlock, heading: HeadingBlock } }, "PageContent");
|
|
165
|
+
|
|
166
|
+
// One-of block
|
|
167
|
+
export const MediaBlock = createOneOfBlock({ supportedBlocks: { image: DamImageBlock, damVideo: DamVideoBlock } }, "Media");
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Keys in `supportedBlocks` must be **camelCase** and **identical** across all layers (API, Admin, Site).
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Naming conventions (API layer)
|
|
175
|
+
|
|
176
|
+
| Element | Convention | Example |
|
|
177
|
+
| ------------------------------------- | ----------------------------------- | ----------------------- |
|
|
178
|
+
| File name | kebab-case ending in `.block.ts` | `product-card.block.ts` |
|
|
179
|
+
| `BlockData` class | `{BlockName}BlockData` | `ProductCardBlockData` |
|
|
180
|
+
| `BlockInput` class | `{BlockName}BlockInput` | `ProductCardBlockInput` |
|
|
181
|
+
| Exported constant | `{BlockName}Block` | `ProductCardBlock` |
|
|
182
|
+
| Block name (3rd arg of `createBlock`) | PascalCase, **no** "Block" suffix | `"ProductCard"` |
|
|
183
|
+
| Enum values | camelCase matching the string value | `left = "left"` |
|