@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,266 @@
|
|
|
1
|
+
# Custom Block Field Rules
|
|
2
|
+
|
|
3
|
+
Detailed rules for creating custom block fields that reference entities using `createCompositeBlockField`. Load this file when a composite block needs a field that selects or references an entity (e.g., picking a product, category, or news article) rather than using a standard block or text field.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
`createCompositeBlockField` is a lower-level helper from `@comet/cms-admin` that creates a custom field for use inside a `createCompositeBlock` `blocks` object. It enables arbitrary admin UI (entity pickers, custom selectors, conditional inputs) while integrating with the block's state management.
|
|
10
|
+
|
|
11
|
+
Use it when the built-in helpers (`createCompositeBlockTextField`, `createCompositeBlockSelectField`, `createCompositeBlockSwitchField`) are insufficient — most commonly when a block needs to store an entity reference (e.g., a product ID selected via an autocomplete search).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## API Side
|
|
16
|
+
|
|
17
|
+
On the API side, the entity reference is stored as a **nullable string field** containing the entity's UUID. The block does not use a relation — it stores a plain ID that the site resolves at render time via a block loader.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
class MyBlockData extends BlockData {
|
|
21
|
+
@BlockField({ nullable: true })
|
|
22
|
+
productId?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class MyBlockInput extends BlockInput {
|
|
26
|
+
@IsUndefinable()
|
|
27
|
+
@IsString()
|
|
28
|
+
@BlockField({ nullable: true })
|
|
29
|
+
productId?: string;
|
|
30
|
+
|
|
31
|
+
transformToBlockData(): MyBlockData {
|
|
32
|
+
return blockInputToData(MyBlockData, this);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Rules:
|
|
38
|
+
|
|
39
|
+
- Use `@BlockField({ nullable: true })` and `@IsUndefinable()` so the block is savable without a selection.
|
|
40
|
+
- Name the field `{entityName}Id` (e.g., `productId`, `categoryId`) to clarify it stores a reference, not the entity itself.
|
|
41
|
+
- The site resolves the ID to full entity data via a block loader (see [block-loader.md](block-loader.md)).
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Admin Side
|
|
46
|
+
|
|
47
|
+
### `createCompositeBlockField` API
|
|
48
|
+
|
|
49
|
+
Import from `@comet/cms-admin`:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { createCompositeBlockField } from "@comet/cms-admin";
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Signature:**
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
createCompositeBlockField<State>(options: Options<State>)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The generic type parameter `State` is the state type for the field — typically a `string | undefined` for entity ID references or a custom object shape.
|
|
62
|
+
|
|
63
|
+
### Options
|
|
64
|
+
|
|
65
|
+
| Option | Type | Required | Description |
|
|
66
|
+
| --------------------- | ------------------------------ | -------- | ----------------------------------------------------------------------------------------------------- |
|
|
67
|
+
| `defaultValue` | `State` | Yes | Default value for new blocks. Use `undefined` for nullable entity references. |
|
|
68
|
+
| `AdminComponent` | `ComponentType<AdminProps>` | Yes | React component receiving `{ state, updateState }` that renders the admin UI for this field. |
|
|
69
|
+
| `definesOwnPadding` | `boolean` | No | Whether the component defines its own padding (skips default block padding). |
|
|
70
|
+
| `extractTextContents` | `(state, options) => string[]` | No | Extracts text content from the state for search indexing. Return entity name or title when available. |
|
|
71
|
+
|
|
72
|
+
The `AdminComponent` receives these props:
|
|
73
|
+
|
|
74
|
+
| Prop | Type | Description |
|
|
75
|
+
| ------------- | --------------------------------- | --------------------------------------------------- |
|
|
76
|
+
| `state` | `State` | Current field state (e.g., the selected entity ID). |
|
|
77
|
+
| `updateState` | `Dispatch<SetStateAction<State>>` | Callback to update the field state. |
|
|
78
|
+
|
|
79
|
+
### Entity Selection Pattern
|
|
80
|
+
|
|
81
|
+
The most common use case is selecting an entity via async search. This combines `BlocksFinalForm`, `AsyncAutocompleteField`, and optionally `useContentScope` for scope-aware queries.
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { AsyncAutocompleteField } from "@comet/admin";
|
|
85
|
+
import { BlocksFinalForm, createCompositeBlockField, useContentScope } from "@comet/cms-admin";
|
|
86
|
+
import { gql, useApolloClient, useQuery } from "@apollo/client";
|
|
87
|
+
import { type ProductPickerBlockData } from "@src/blocks.generated";
|
|
88
|
+
import { FormattedMessage } from "react-intl";
|
|
89
|
+
|
|
90
|
+
const productPickerBlockProductSelectQuery = gql`
|
|
91
|
+
query ProductPickerBlockProductSelect($scope: ProductContentScopeInput!, $search: String) {
|
|
92
|
+
products(scope: $scope, search: $search) {
|
|
93
|
+
nodes {
|
|
94
|
+
id
|
|
95
|
+
name
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
export const ProductPickerBlock = createCompositeBlock(
|
|
102
|
+
{
|
|
103
|
+
name: "ProductPicker",
|
|
104
|
+
displayName: <FormattedMessage id="productPickerBlock.displayName" defaultMessage="Product Picker" />,
|
|
105
|
+
blocks: {
|
|
106
|
+
productId: {
|
|
107
|
+
block: createCompositeBlockField<ProductPickerBlockData["productId"]>({
|
|
108
|
+
defaultValue: undefined,
|
|
109
|
+
AdminComponent: ({ state, updateState }) => {
|
|
110
|
+
const client = useApolloClient();
|
|
111
|
+
const { scope } = useContentScope();
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<BlocksFinalForm<{ productId: typeof state }>
|
|
115
|
+
onSubmit={({ productId }) => updateState(productId)}
|
|
116
|
+
initialValues={{ productId: state }}
|
|
117
|
+
>
|
|
118
|
+
<AsyncAutocompleteField
|
|
119
|
+
name="productId"
|
|
120
|
+
label={<FormattedMessage id="productPickerBlock.product" defaultMessage="Product" />}
|
|
121
|
+
fullWidth
|
|
122
|
+
loadOptions={async (search?: string) => {
|
|
123
|
+
const { data } = await client.query({
|
|
124
|
+
query: productPickerBlockProductSelectQuery,
|
|
125
|
+
variables: { scope, search },
|
|
126
|
+
});
|
|
127
|
+
return data.products.nodes;
|
|
128
|
+
}}
|
|
129
|
+
getOptionLabel={(option) => option.name}
|
|
130
|
+
/>
|
|
131
|
+
</BlocksFinalForm>
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
title: <FormattedMessage id="productPickerBlock.product" defaultMessage="Product" />,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
(block) => {
|
|
140
|
+
block.previewContent = (state) => [{ type: "text", content: state.productId ?? "" }];
|
|
141
|
+
return block;
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Key components
|
|
147
|
+
|
|
148
|
+
**`BlocksFinalForm`** — Wraps `react-final-form` with auto-save behavior (submits on every change via `FormSpy`, no save button). Always use it inside `AdminComponent` to bridge block state and form fields.
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
<BlocksFinalForm<FormValues> onSubmit={(values) => updateState(values.fieldName)} initialValues={{ fieldName: state }}>
|
|
152
|
+
{/* form fields */}
|
|
153
|
+
</BlocksFinalForm>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- The generic type parameter provides type safety for form values.
|
|
157
|
+
- `onSubmit` fires automatically whenever a form field changes.
|
|
158
|
+
- `initialValues` should reflect the current block state.
|
|
159
|
+
|
|
160
|
+
**`AsyncAutocompleteField`** — Async entity search field with type-ahead. Import from `@comet/admin`.
|
|
161
|
+
|
|
162
|
+
**`useContentScope`** — Hook from `@comet/cms-admin` that provides the current content scope (domain, language). Use it when the entity query requires scope filtering.
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
const { scope } = useContentScope();
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Pass `scope` as a variable to GraphQL queries so entity search results match the active content scope.
|
|
169
|
+
|
|
170
|
+
### Showing the selected entity
|
|
171
|
+
|
|
172
|
+
When the field should display details about the currently selected entity (not just store the ID), use `useQuery` to load entity data based on the stored ID:
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
AdminComponent: ({ state, updateState }) => {
|
|
176
|
+
const client = useApolloClient();
|
|
177
|
+
const { scope } = useContentScope();
|
|
178
|
+
|
|
179
|
+
const { data: selectedProductData } = useQuery(
|
|
180
|
+
gql`
|
|
181
|
+
query ProductPickerBlockSelectedProduct($id: ID!) {
|
|
182
|
+
product(id: $id) {
|
|
183
|
+
id
|
|
184
|
+
name
|
|
185
|
+
visible
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
`,
|
|
189
|
+
{ variables: { id: state }, skip: !state },
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const selectedProduct = selectedProductData?.product;
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<BlocksFinalForm<{ productId: typeof state }>
|
|
196
|
+
onSubmit={({ productId }) => updateState(productId)}
|
|
197
|
+
initialValues={{ productId: state }}
|
|
198
|
+
>
|
|
199
|
+
<AsyncAutocompleteField
|
|
200
|
+
name="productId"
|
|
201
|
+
label={<FormattedMessage id="productPickerBlock.product" defaultMessage="Product" />}
|
|
202
|
+
fullWidth
|
|
203
|
+
loadOptions={async (search?: string) => {
|
|
204
|
+
const { data } = await client.query({
|
|
205
|
+
query: productPickerBlockProductSelectQuery,
|
|
206
|
+
variables: { scope, search },
|
|
207
|
+
});
|
|
208
|
+
return data.products.nodes;
|
|
209
|
+
}}
|
|
210
|
+
getOptionLabel={(option) => option.name}
|
|
211
|
+
/>
|
|
212
|
+
{selectedProduct && !selectedProduct.visible && (
|
|
213
|
+
<Alert severity="warning">
|
|
214
|
+
<FormattedMessage
|
|
215
|
+
id="productPickerBlock.hiddenWarning"
|
|
216
|
+
defaultMessage="The selected product is not visible and will not appear on the site."
|
|
217
|
+
/>
|
|
218
|
+
</Alert>
|
|
219
|
+
)}
|
|
220
|
+
</BlocksFinalForm>
|
|
221
|
+
);
|
|
222
|
+
},
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Use `skip: !state` to avoid querying when no entity is selected. Include `visible` (or `status`) in the query to detect hidden entities and warn the editor.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Site Side
|
|
230
|
+
|
|
231
|
+
The site receives the stored entity ID from the block data. Use a **block loader** to resolve the ID to full entity data at render time. Always filter out hidden or unpublished entities in the loader — never pass invisible entity data to the client.
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
import { type PropsWithData, withPreview } from "@comet/cms-site";
|
|
235
|
+
import { type ProductPickerBlockData } from "@src/blocks.generated";
|
|
236
|
+
|
|
237
|
+
interface ProductPickerBlockLoaderData {
|
|
238
|
+
product: { id: string; name: string; imageUrl: string } | null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export const ProductPickerBlock = withPreview(
|
|
242
|
+
({ data }: PropsWithData<ProductPickerBlockData & { loaded: ProductPickerBlockLoaderData }>) => {
|
|
243
|
+
const { loaded } = data;
|
|
244
|
+
|
|
245
|
+
if (!loaded.product) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return <div>{loaded.product.name}</div>;
|
|
250
|
+
},
|
|
251
|
+
{ label: "Product Picker" },
|
|
252
|
+
);
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
When `loaded.product` is `null` (entity deleted, unpublished, or ID missing), render nothing.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Common Pitfalls
|
|
260
|
+
|
|
261
|
+
1. **Forgetting `@IsUndefinable()` on the API field** — without it, the block cannot be saved in its initial empty state because validation rejects `undefined`.
|
|
262
|
+
2. **Not using `skip: !state` in `useQuery`** — querying with an undefined ID causes a GraphQL error.
|
|
263
|
+
3. **Storing the entire entity object instead of just the ID** — block data is stored as JSON in the database. Store only the entity's UUID; resolve full data at render time via a block loader.
|
|
264
|
+
4. **Not filtering by scope in `loadOptions`** — in multi-scope projects, entity search results must match the current content scope so editors only see relevant entities.
|
|
265
|
+
5. **Not warning about hidden entities** — when the selected entity is unpublished or invisible, show a warning in the admin so editors know the block will render empty on the site.
|
|
266
|
+
6. **Using `useQuery` inside `loadOptions`** — `loadOptions` is an async callback, not a React component. Use `client.query()` (from `useApolloClient`) for imperative queries inside callbacks; use `useQuery` only for declarative queries in the component body.
|