@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.
Files changed (35) hide show
  1. package/LICENSE +24 -0
  2. package/package.json +19 -0
  3. package/rules/coding-guidelines/api-nestjs.instructions.md +107 -0
  4. package/rules/coding-guidelines/cdn.instructions.md +24 -0
  5. package/rules/coding-guidelines/general.instructions.md +30 -0
  6. package/rules/coding-guidelines/git.instructions.md +37 -0
  7. package/rules/coding-guidelines/kubernetes.instructions.md +59 -0
  8. package/rules/coding-guidelines/libraries.instructions.md +34 -0
  9. package/rules/coding-guidelines/naming.instructions.md +39 -0
  10. package/rules/coding-guidelines/postgresql.instructions.md +40 -0
  11. package/rules/coding-guidelines/react.instructions.md +102 -0
  12. package/rules/coding-guidelines/security.instructions.md +44 -0
  13. package/rules/coding-guidelines/styling.instructions.md +50 -0
  14. package/rules/coding-guidelines/typescript.instructions.md +50 -0
  15. package/skills/.gitkeep +0 -0
  16. package/skills/comet-block/SKILL.md +246 -0
  17. package/skills/comet-block/references/admin-patterns.md +192 -0
  18. package/skills/comet-block/references/api-patterns.md +183 -0
  19. package/skills/comet-block/references/block-loader.md +368 -0
  20. package/skills/comet-block/references/block-types.md +210 -0
  21. package/skills/comet-block/references/custom-block-field.md +266 -0
  22. package/skills/comet-block/references/fixtures.md +436 -0
  23. package/skills/comet-block/references/image.md +341 -0
  24. package/skills/comet-block/references/migration.md +597 -0
  25. package/skills/comet-block/references/registration.md +167 -0
  26. package/skills/comet-block/references/response-summary.md +102 -0
  27. package/skills/comet-block/references/rich-text.md +309 -0
  28. package/skills/comet-block/references/select.md +176 -0
  29. package/skills/comet-block/references/site-patterns.md +202 -0
  30. package/skills/comet-mail-react/SKILL.md +539 -0
  31. package/skills/comet-mail-react/references/components-and-theme.md +395 -0
  32. package/skills/comet-mail-react/references/layout-patterns.md +315 -0
  33. package/skills/comet-mail-react/references/styling-and-customization.md +306 -0
  34. package/skills/comet-minor-update/SKILL.md +191 -0
  35. 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.