Skip to content

Commit 007310f

Browse files
committed
chore(spec): changeset + regenerated artifacts for the widened related-list columns
`check:generated` proved exactly two artifacts stale and `--fix` regenerated those two, never the whole set: `api-surface-declarations/ui.txt` (the declaration text of `RecordRelatedListProps` and `ComponentPropsMap`) and `content/docs/references/ui/component.mdx`, which now renders the nested `RecordRelatedListProps.columns[number]` shape the union admits. Changeset: `@objectstack/spec` minor, `Clause-②: yes (widening)`. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0f09146 commit 007310f

3 files changed

Lines changed: 171 additions & 3 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
`record:related_list.columns` now declares the SAME union the saved-view key declares — `z.union([z.array(z.string()), z.array(ListColumnSchema)])` — so a saved view's per-column decoration reaches the related list instead of being refused at the block door (#18639, the upstream half of objectui#9593).
6+
7+
**Clause-②: yes (widening)** — one published accept set grows: the key admitted `string[]` and now also admits `ListColumn[]`. Nothing previously admitted is refused, no key is renamed or retired, and no producer is required to write the new arm. Contract-review tier.
8+
9+
Two published declarations disagreed about one key. `RecordRelatedListProps.columns` (`ui/component.zod.ts`) was `z.array(z.string())`, while `listViews[].columns` (`ui/view.zod.ts`) was already the union — and objectui composes a saved view's `columns` onto this block **verbatim** (`dataSource.view` → `composeElementDataSource` → `savedViewColumns`). A view whose columns carried `label` / `width` / `hidden` / `summary` therefore arrived at a block that declared it could not carry them.
10+
11+
- **The same union, by reference — not a lookalike.** `ListColumnSchema` is imported from the view face rather than re-spelled, so the object arm is one def with two carriers. The pin asserts reference identity on both sides and then asserts block and saved view return the same verdict for every fixture: two spellings of one key is the defect this closes, so a second spelling would not have fixed it.
12+
- **The arms are exclusive, and the description says so because the schema enforces it.** `['name', { field: 'amount' }]` matches neither arm and is refused. The decoration also survives the parse — a description promising keys a parse strips would be the same defect one layer up, so the pin asserts the parsed value, not merely `success`.
13+
- **Unchanged, by ruling and by measurement.** `field.relatedListColumns` stays child field-name STRINGS only and still refuses a column object with its derivation prescription, and the `field-column-lists-canonicalized` conversion still folds an object entry on that key to its identity string. Both are pinned next to the widening so the fences cannot erode quietly.
14+
15+
No migration: authors writing `string[]` are unaffected, and the new arm is opt-in.

‎content/docs/references/ui/component.mdx‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1365,7 +1365,7 @@ Type: `string`
13651365
| **objectName** | `string` | ✅ | Related object name (e.g., "task", "opportunity") |
13661366
| **relationshipField** | `string` | ✅ | Field on related object that points to this record (e.g., "account_id") |
13671367
| **relationshipValueField** | `string` | optional (default: `"id"`) | Parent-record field whose value relationshipField stores (default 'id'; e.g. 'name' for name-keyed junctions). |
1368-
| **columns** | `string[]` | optional | Fields to display in the related list. Optional: when omitted, columns derive from the related object's highlightFields / default list columns (a related list is just another surface that lists that object). Override chain: child highlightFields → field-level relatedListColumns → this inline list. |
1368+
| **columns** | `string[] \| { field: string; label?: string \| Record<string, string>; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | optional | Fields to display in the related list — either plain field-name strings, or the same per-column entries a saved list view declares (`ListColumn`: `field`, plus `label`, `width`, `align`, `hidden`, `sortable`, `summary`, …). A view-supplied list may arrive in the `ListColumn` spelling: objectui composes a saved view's `columns` onto this block verbatim, and this key declares the SAME union as `listViews[].columns`. One spelling per list — the two arms are exclusive, so an array mixing strings and column objects is refused. Optional: when omitted, columns derive from the related object's highlightFields / default list columns (a related list is just another surface that lists that object). Override chain: child highlightFields → field-level relatedListColumns (field-name strings only) → this inline list. |
13691369
| **sort** | `string \| { field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Sort order for related records |
13701370
| **limit** | `integer` | optional (default: `5`) | Number of records to display initially |
13711371
| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Additional filter criteria for related records |
@@ -1377,6 +1377,25 @@ Type: `string`
13771377
| **redactFields** | `string[]` | optional | Field names this list never renders, whatever the permission answer (renderer default: render every column authored or derived). Applies to the authored `columns` AND to the columns the list derives for itself when none are authored. Presentation only, evaluated in the browser after the rows are fetched — the values are still in the page, so this is NOT a data-access control and NOT the object's `publicSharing.redactFields`, which removes them server-side. To keep a value from the caller, gate the field itself (`requiredPermissions` / `maskingRule`, ADR-0066 D3) or the permission set. |
13781378
| **aria** | `{ ariaLabel?: string \| Record<string, string>; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes |
13791379

1380+
### Nested Shape: `RecordRelatedListProps.columns[number]`
1381+
1382+
| Property | Type | Required | Description |
1383+
| :--- | :--- | :--- | :--- |
1384+
| **field** | `string` | ✅ | Field name (snake_case) |
1385+
| **label** | `string \| Record<string, string>` | optional | Display label override |
1386+
| **width** | `number` | optional | Column width in pixels |
1387+
| **align** | `Enum<'left' \| 'center' \| 'right'>` | optional | Text alignment |
1388+
| **hidden** | `boolean` | optional | Hide column by default |
1389+
| **sortable** | `boolean` | optional | Allow sorting by this column |
1390+
| **resizable** | `boolean` | optional | Allow resizing this column |
1391+
| **wrap** | `boolean` | optional | Allow text wrapping |
1392+
| **type** | `string` | optional | Renderer type override (e.g., "currency", "date") |
1393+
| **pinned** | `Enum<'left' \| 'right'>` | optional | Pin/freeze column to left or right side |
1394+
| **summary** | `Enum<'none' \| 'count' \| 'count_empty' \| 'count_filled' \| 'count_unique' \| …> \| { type: Enum<'none' \| 'count' \| 'count_empty' \| 'count_filled' \| 'count_unique' \| …>; field?: string }` | optional | Footer aggregation for this column — the function alone, or `{ type, field }` to aggregate another field |
1395+
| **prefix** | `{ field: string; type: Enum<'badge' \| 'text'> }` | optional | Field rendered inline before this cell value |
1396+
| **link** | `boolean` | optional | Functions as the primary navigation link (triggers View navigation) |
1397+
| **action** | `string` | optional | Registered Action ID to execute when clicked |
1398+
13801399
### Nested Shape: `RecordRelatedListProps.filter[number]`
13811400

13821401
View filter rule

‎packages/spec/api-surface-declarations/ui.txt‎

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3097,7 +3097,74 @@ declare const ComponentPropsMap: {
30973097
objectName: z.ZodString;
30983098
relationshipField: z.ZodString;
30993099
relationshipValueField: z.ZodDefault<z.ZodString>;
3100-
columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
3100+
columns: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodArray<z.ZodObject<{
3101+
field: z.ZodString;
3102+
label: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodType<Record<string, string> & {
3103+
key?: never;
3104+
defaultValue?: never;
3105+
}, Record<string, string> & {
3106+
key?: never;
3107+
defaultValue?: never;
3108+
}, z.core.$ZodTypeInternals<Record<string, string> & {
3109+
key?: never;
3110+
defaultValue?: never;
3111+
}, Record<string, string> & {
3112+
key?: never;
3113+
defaultValue?: never;
3114+
}>>]>>;
3115+
width: z.ZodOptional<z.ZodNumber>;
3116+
align: z.ZodOptional<z.ZodEnum<{
3117+
left: "left";
3118+
right: "right";
3119+
center: "center";
3120+
}>>;
3121+
hidden: z.ZodOptional<z.ZodBoolean>;
3122+
sortable: z.ZodOptional<z.ZodBoolean>;
3123+
resizable: z.ZodOptional<z.ZodBoolean>;
3124+
wrap: z.ZodOptional<z.ZodBoolean>;
3125+
type: z.ZodOptional<z.ZodString>;
3126+
pinned: z.ZodOptional<z.ZodEnum<{
3127+
left: "left";
3128+
right: "right";
3129+
}>>;
3130+
summary: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
3131+
count: "count";
3132+
none: "none";
3133+
min: "min";
3134+
max: "max";
3135+
sum: "sum";
3136+
avg: "avg";
3137+
count_empty: "count_empty";
3138+
count_filled: "count_filled";
3139+
count_unique: "count_unique";
3140+
percent_empty: "percent_empty";
3141+
percent_filled: "percent_filled";
3142+
}>, z.ZodObject<{
3143+
type: z.ZodEnum<{
3144+
count: "count";
3145+
none: "none";
3146+
min: "min";
3147+
max: "max";
3148+
sum: "sum";
3149+
avg: "avg";
3150+
count_empty: "count_empty";
3151+
count_filled: "count_filled";
3152+
count_unique: "count_unique";
3153+
percent_empty: "percent_empty";
3154+
percent_filled: "percent_filled";
3155+
}>;
3156+
field: z.ZodOptional<z.ZodString>;
3157+
}, z.core.$strict>]>>;
3158+
prefix: z.ZodOptional<z.ZodObject<{
3159+
field: z.ZodString;
3160+
type: z.ZodDefault<z.ZodEnum<{
3161+
text: "text";
3162+
badge: "badge";
3163+
}>>;
3164+
}, z.core.$strict>>;
3165+
link: z.ZodOptional<z.ZodBoolean>;
3166+
action: z.ZodOptional<z.ZodString>;
3167+
}, z.core.$strict>>]>>;
31013168
sort: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodObject<{
31023169
field: z.ZodString;
31033170
order: z.ZodEnum<{
@@ -19220,7 +19287,74 @@ declare const RecordRelatedListProps: z.ZodObject<{
1922019287
objectName: z.ZodString;
1922119288
relationshipField: z.ZodString;
1922219289
relationshipValueField: z.ZodDefault<z.ZodString>;
19223-
columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
19290+
columns: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodArray<z.ZodObject<{
19291+
field: z.ZodString;
19292+
label: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodType<Record<string, string> & {
19293+
key?: never;
19294+
defaultValue?: never;
19295+
}, Record<string, string> & {
19296+
key?: never;
19297+
defaultValue?: never;
19298+
}, z.core.$ZodTypeInternals<Record<string, string> & {
19299+
key?: never;
19300+
defaultValue?: never;
19301+
}, Record<string, string> & {
19302+
key?: never;
19303+
defaultValue?: never;
19304+
}>>]>>;
19305+
width: z.ZodOptional<z.ZodNumber>;
19306+
align: z.ZodOptional<z.ZodEnum<{
19307+
left: "left";
19308+
right: "right";
19309+
center: "center";
19310+
}>>;
19311+
hidden: z.ZodOptional<z.ZodBoolean>;
19312+
sortable: z.ZodOptional<z.ZodBoolean>;
19313+
resizable: z.ZodOptional<z.ZodBoolean>;
19314+
wrap: z.ZodOptional<z.ZodBoolean>;
19315+
type: z.ZodOptional<z.ZodString>;
19316+
pinned: z.ZodOptional<z.ZodEnum<{
19317+
left: "left";
19318+
right: "right";
19319+
}>>;
19320+
summary: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
19321+
count: "count";
19322+
none: "none";
19323+
min: "min";
19324+
max: "max";
19325+
sum: "sum";
19326+
avg: "avg";
19327+
count_empty: "count_empty";
19328+
count_filled: "count_filled";
19329+
count_unique: "count_unique";
19330+
percent_empty: "percent_empty";
19331+
percent_filled: "percent_filled";
19332+
}>, z.ZodObject<{
19333+
type: z.ZodEnum<{
19334+
count: "count";
19335+
none: "none";
19336+
min: "min";
19337+
max: "max";
19338+
sum: "sum";
19339+
avg: "avg";
19340+
count_empty: "count_empty";
19341+
count_filled: "count_filled";
19342+
count_unique: "count_unique";
19343+
percent_empty: "percent_empty";
19344+
percent_filled: "percent_filled";
19345+
}>;
19346+
field: z.ZodOptional<z.ZodString>;
19347+
}, z.core.$strict>]>>;
19348+
prefix: z.ZodOptional<z.ZodObject<{
19349+
field: z.ZodString;
19350+
type: z.ZodDefault<z.ZodEnum<{
19351+
text: "text";
19352+
badge: "badge";
19353+
}>>;
19354+
}, z.core.$strict>>;
19355+
link: z.ZodOptional<z.ZodBoolean>;
19356+
action: z.ZodOptional<z.ZodString>;
19357+
}, z.core.$strict>>]>>;
1922419358
sort: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodObject<{
1922519359
field: z.ZodString;
1922619360
order: z.ZodEnum<{

0 commit comments

Comments
 (0)