Skip to content

Commit 00bd2da

Browse files
committed
fix(spec): the 18.x action-block variant entry no longer says element:button keeps destructive
element:button spells the destructive arm `danger` and refuses `destructive`; only the record blocks that forward a variant to the Button primitive unchanged (record:quick_actions, a record:alert action) still take the primitive's `destructive`. Corrects the D3 entry's reason (printed by the upgrade guide), the D2 conversion's docblock and the conversion test's wording, and pins the corrected sentence against the rows it describes. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzjAFu9q3UWTLiMSuk9
1 parent 178b523 commit 00bd2da

4 files changed

Lines changed: 76 additions & 9 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): the 18.x migration entry for the action-block `destructive` → `danger` rename no longer says `element:button` keeps `destructive` (#27589)
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The D3 entry `ui-page-action-block-variant-destructive-refused` said that components drawing the Button primitive directly, "`element:button` and the like", keep `destructive`. That is false for `element:button`: `ElementButtonPropsSchema.variant` is `primary | secondary | danger | ghost | link`, so it already spells the arm `danger` and refuses `destructive` at the metadata save door and in `os validate`. An author who followed the sentence wrote a page the save door rejects. The entry's `reason`, which the upgrade guide prints, now says so, and names the two record blocks that do still take the React Button primitive's `destructive` because they forward a variant to it unchanged: `record:quick_actions` (`variant`) and a `record:alert` action (`action.variant`). The same correction is made to the docblock of the D2 conversion `page-action-block-variant-destructive-to-danger`. No schema, value or conversion behaviour changes.
11+
12+
**Supersedes, in the same release.** The `@objectstack/spec` 18 note for the rename says "Other components that draw the Button primitive directly (`element:button`, toasts, alerts, badges and the like) keep `destructive`". Read `element:button` out of that list: it spells the arm `danger`, as the action blocks now do.

‎packages/spec/src/conversions/page-action-block-variant-destructive-to-danger.test.ts‎

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ import {
77
ActionGroupPropsSchema,
88
ActionIconPropsSchema,
99
ActionMenuPropsSchema,
10+
ElementButtonPropsSchema,
11+
RecordAlertActionSchema,
12+
RecordQuickActionsProps,
1013
} from '../ui/component.zod.js';
1114
import { ActionSchema } from '../ui/action.zod.js';
15+
import { MIGRATIONS_BY_MAJOR } from '../migrations/registry.js';
1216
import { applyConversions } from './apply.js';
1317
import { ALL_CONVERSIONS } from './registry.js';
1418
import { applyConversionsToStoredItem } from './stored.js';
@@ -33,8 +37,11 @@ import type { ConversionNotice } from './types.js';
3337
* - the authoring funnel does NOT replay it;
3438
* - the same seam renames an `action:bar`'s own `variant` and its members',
3539
* though the bar has no row: it draws its members through the blocks;
36-
* - another component's `destructive` (the Button primitive's own word) and
37-
* every other variant are left byte-identical with no notice;
40+
* - another component type's `variant`, even `destructive`, and every other
41+
* variant are left byte-identical with no notice;
42+
* - the D3 entry's text says what the rows measure: `element:button` spells
43+
* the arm `danger` and refuses `destructive`, and only the record blocks
44+
* that forward to the Button primitive still take its `destructive`;
3845
* - idempotence, and copy-on-write identity for a page with nothing to do.
3946
*/
4047
const ID = 'page-action-block-variant-destructive-to-danger';
@@ -177,7 +184,7 @@ describe('page-action-block-variant-destructive-to-danger (ADR-0087 D2)', () =>
177184
it.each([
178185
['the canonical `danger`', { type: 'action:button', properties: { label: 'Purge', variant: 'danger' } }],
179186
['another variant', { type: 'action:icon', properties: { icon: 'copy', variant: 'outline' } }],
180-
["an `element:button`'s `destructive` (the Button primitive's own word)", { type: 'element:button', properties: { label: 'Reset', variant: 'destructive' } }],
187+
["another component type's `destructive` (an `element:button`, whose own row refuses it)", { type: 'element:button', properties: { label: 'Reset', variant: 'destructive' } }],
181188
["a `destructive` member on a block that has no members", { type: 'action:button', properties: { label: 'Go', actions: [{ variant: 'destructive' }] } }],
182189
['a member that is not an object', { type: 'action:menu', properties: { actions: ['destructive'] } }],
183190
['`actions` that is not a list', { type: 'action:group', properties: { actions: { variant: 'destructive' } } }],
@@ -216,3 +223,47 @@ describe('page-action-block-variant-destructive-to-danger (ADR-0087 D2)', () =>
216223
expect(notices.filter((n) => n.conversionId === ID)).toEqual([]);
217224
});
218225
});
226+
227+
/**
228+
* The D3 entry's `reason` is compiled into the 18.x upgrade guide, so its
229+
* sentence about the components this rename leaves alone is pinned against
230+
* the rows it describes (#27589). It once said `element:button` keeps
231+
* `destructive`, a value that row refuses: an author who followed the guide
232+
* wrote a page the save door rejects.
233+
*/
234+
describe('the D3 entry says what the untouched components accept (#27589)', () => {
235+
// Read from the generated chain, the source `build-upgrade-guide.ts` prints.
236+
const refusedEntry = MIGRATIONS_BY_MAJOR[18]!.semantic.find(
237+
(s) => s.id === 'ui-page-action-block-variant-destructive-refused',
238+
)!;
239+
240+
it('is registered on the protocol 18 step', () => {
241+
expect(refusedEntry, 'the D3 entry is on step 18').toBeDefined();
242+
});
243+
244+
it('`element:button` spells the arm `danger` and refuses `destructive`', () => {
245+
expect(ElementButtonPropsSchema.safeParse({ label: 'Reset', variant: 'danger' }).success).toBe(true);
246+
expect(ElementButtonPropsSchema.safeParse({ label: 'Reset', variant: 'destructive' }).success).toBe(false);
247+
expect(refusedEntry.reason).toContain(
248+
'`element:button` already spells it `danger` (`ElementButtonPropsSchema.variant`) and refuses `destructive`, '
249+
+ 'so it is not touched.',
250+
);
251+
});
252+
253+
it('the record blocks that forward to the Button primitive still take its `destructive`', () => {
254+
expect(RecordQuickActionsProps.safeParse({ actionNames: ['purge'], variant: 'destructive' }).success).toBe(true);
255+
expect(RecordAlertActionSchema.safeParse({ actionName: 'purge', variant: 'destructive' }).success).toBe(true);
256+
expect(refusedEntry.reason).toContain(
257+
"`destructive` is the React Button primitive's own word; the two record blocks that forward a variant to that "
258+
+ 'primitive unchanged, `record:quick_actions` and a `record:alert` action, still take it and are not touched '
259+
+ 'either.',
260+
);
261+
});
262+
263+
it('no longer tells an author that `element:button` keeps `destructive`', () => {
264+
for (const text of [refusedEntry.reason, refusedEntry.replacement, refusedEntry.acceptanceCriteria]) {
265+
expect(text).not.toMatch(/keeps? `destructive`/);
266+
expect(text).not.toContain('`element:button` and the like');
267+
}
268+
});
269+
});

‎packages/spec/src/conversions/registry.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11530,9 +11530,11 @@ const ACTION_CONTAINER_BLOCK_TYPES = new Set(['action:group', 'action:menu']);
1153011530
* longer read that word, and would fall back to a plain button with no
1153111531
* refusal anywhere to say so. Its own `variant` and its members' are renamed
1153211532
* as the four blocks' are. Every other value is left alone, and so is every
11533-
* other component type: `destructive` stays the Button primitive's word on
11534-
* `element:button` and the other primitive-backed components, which this
11535-
* entry never reads. A member that is not an object, or a list that is not an
11533+
* other component type, which this entry never reads: `element:button`
11534+
* already spells the arm `danger` and refuses `destructive`, and the record
11535+
* blocks that forward a variant to the Button primitive unchanged
11536+
* (`record:quick_actions`, a `record:alert` action) keep the primitive's own
11537+
* `destructive`. A member that is not an object, or a list that is not an
1153611538
* array, is left as stored. The reach is every position `mapPageComponents`
1153711539
* visits — regions, slots, nested containers — the props gate's own reach.
1153811540
* Idempotent by construction: a rewritten value reads `'danger'`, which this
@@ -11607,7 +11609,7 @@ const pageActionBlockVariantDestructiveToDanger: MetadataConversion = {
1160711609
// Canonical already, and another value: untouched.
1160811610
{ type: 'action:button', properties: { label: 'Archive', actionType: 'script', variant: 'danger' } },
1160911611
{ type: 'action:button', properties: { label: 'Open', actionType: 'url', target: '/apps/ops', variant: 'outline' } },
11610-
// The Button primitive's own word on another component: not this entry's.
11612+
// Another component type: not this entry's, whatever it holds (its own row refuses `destructive`).
1161111613
{ type: 'element:button', properties: { label: 'Reset', variant: 'destructive' } },
1161211614
// A container's trigger and one of its members: both renamed, the other member untouched.
1161311615
{

‎packages/spec/src/migrations/entries/semantic/18.ui-page-action-block-variant-destructive-refused.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ export const entry: SemanticMigration = {
1616
+ "dispatches, says `danger`; the action block rows took the Button primitive's word, `destructive`, and "
1717
+ 'refused `danger`, so one destructive action changed spelling between a declaration and a block, and each '
1818
+ 'block renderer translated between the two at its forward to the runner. The blocks now take `danger` and '
19-
+ 'refuse `destructive` with the rename. Other components that draw the Button primitive directly '
20-
+ "(`element:button` and the like) keep `destructive`, the primitive's own word, and are not touched. A page's "
19+
+ 'refuse `destructive` with the rename. `element:button` already spells it `danger` '
20+
+ '(`ElementButtonPropsSchema.variant`) and refuses `destructive`, so it is not touched. `destructive` is the '
21+
+ "React Button primitive's own word; the two record blocks that forward a variant to that primitive unchanged, "
22+
+ "`record:quick_actions` and a `record:alert` action, still take it and are not touched either. A page's "
2123
+ '`properties` is not parsed on the metadata load path, and the next save of the page meets the '
2224
+ 'component-props gate at the metadata save door; `os validate` reports the old value as a '
2325
+ '`component-props-invalid` finding. The D2 conversion `page-action-block-variant-destructive-to-danger` '

0 commit comments

Comments
 (0)