Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/11985-batch-aborted-banner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@object-ui/app-shell': patch
---

A rolled-back Studio publish shows its "Nothing was published" banner again (objectui#11985).

When a package publish fails, the server rolls the whole batch back and marks every draft that went down with the failing one with the code `BATCH_ABORTED`. Studio's publish message compared that code to a lower-case `batch_aborted`, so the match never happened: instead of the banner, the cause and a count of the aborted drafts, the author got one parallel line per draft, each sibling reading as if it had failed on its own. Studio's publish toasts and the AI chat page's publish summary now match `BATCH_ABORTED` exactly as the server sends it, so they lead with the banner, name the item that caused the rollback and count the rest. A publish that fails without rolling anything back still lists each failed draft with its own error.

Nothing is added to the package entry: no export, prop, type member or language-pack key.
11 changes: 5 additions & 6 deletions packages/app-shell/src/preview/publishRefusal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@
* When every element is aborted (the producer could not attribute the
* failure), the first one's sentence is still the server's own.
*
* Not `formatPublishFailures` (`studio-design/metadataError.ts`), and measured
* rather than assumed: it tells a rolled-back sibling by the pre-ADR-0112
* spelling `batch_aborted` compared with `===`, so on the producer's
* `BATCH_ABORTED` it lists every sibling as a parallel error and never reaches
* its own rolled-back banner; and its banner is English, where the package
* sheet answers in its own localized rows. `errorCodeIs` matches both spellings.
* Not `formatPublishFailures` (`studio-design/metadataError.ts`): its banner is
* English, where the package sheet answers in its own localized rows. The two
* also tell a rolled-back sibling apart differently: `errorCodeIs` here ignores
* case, while `formatPublishFailures` matches the producer's `BATCH_ABORTED`
* exactly (objectui#11985).
*/

import { errorCodeIs } from '@object-ui/types';
Expand Down
51 changes: 45 additions & 6 deletions packages/app-shell/src/views/studio-design/metadataError.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ describe('formatPublishFailures', () => {
});

// framework 15.1+ (ADR-0067 D2) — the batch is all-or-nothing; `failed[]`
// carries the causal item + batch_aborted markers for the rolled-back rest.
// carries the causal item + BATCH_ABORTED markers for the rolled-back rest.
it('15.1+ all-or-nothing: one rolled-back banner anchored on the causal item', () => {
const out = formatPublishFailures([
{ type: 'object', name: 'crm_lead', error: 'not published — the batch is all-or-nothing…', code: 'batch_aborted' },
{ type: 'object', name: 'crm_lead', error: 'not published — the batch is all-or-nothing…', code: 'BATCH_ABORTED' },
{
type: 'object', name: 'crm_deal', error: 'failed spec validation', code: 'invalid_metadata',
type: 'object', name: 'crm_deal', error: 'failed spec validation', code: 'INVALID_METADATA',
issues: [{ path: 'fields.amount.type', message: 'Required' }],
},
{ type: 'view', name: 'lead_list', error: 'not published — …', code: 'batch_aborted' },
{ type: 'view', name: 'lead_list', error: 'not published — …', code: 'BATCH_ABORTED' },
]);
expect(out).toContain('Nothing was published — the batch rolled back');
// causal item with its real error and field-anchored issues…
Expand All @@ -45,10 +45,49 @@ describe('formatPublishFailures', () => {

it('all entries aborted (defensive): banner still renders with one sample', () => {
const out = formatPublishFailures([
{ type: 'object', name: 'a', error: 'not published — …', code: 'batch_aborted' },
{ type: 'object', name: 'b', error: 'not published — …', code: 'batch_aborted' },
{ type: 'object', name: 'a', error: 'not published — …', code: 'BATCH_ABORTED' },
{ type: 'object', name: 'b', error: 'not published — …', code: 'BATCH_ABORTED' },
]);
expect(out).toContain('Nothing was published');
expect(out).toContain('object/a');
});
});

// objectui#11985 — the fixtures above once spelled the sibling code
// `batch_aborted`, a spelling `publishPackageDrafts` never emits, so the banner
// branch was green in tests and dead in Studio. These two cases use the
// producer's rollback answer verbatim: a causal item, then each sibling with
// the producer's own sentence and `code: 'BATCH_ABORTED'`.
describe('formatPublishFailures on the producer rollback answer (objectui#11985)', () => {
const sibling = (type: string, name: string) => ({
type,
name,
error:
'not published — the batch is all-or-nothing (ADR-0067 D2) and object/lead failed; the transaction rolled back',
code: 'BATCH_ABORTED',
});

it('a rolled-back batch leads with the banner, names the cause and counts the aborted drafts', () => {
const out = formatPublishFailures([
{ type: 'object', name: 'lead', error: 'failed spec validation', code: 'INVALID_METADATA' },
sibling('view', 'lead_list'),
sibling('page', 'lead_home'),
]);
const lines = out.split('\n');
expect(lines).toHaveLength(3);
expect(lines[0]).toContain('Nothing was published');
expect(lines[1]).toBe('object/lead: failed spec validation');
expect(lines[2]).toContain('2 other drafts aborted');
// The siblings are counted, not listed as parallel errors.
expect(out).not.toContain('view/lead_list');
expect(out).not.toContain('page/lead_home');
});

it('control: a causal failure with no aborted sibling keeps its own error and no banner', () => {
const out = formatPublishFailures([
{ type: 'object', name: 'lead', error: 'failed spec validation', code: 'INVALID_METADATA' },
]);
expect(out).not.toContain('Nothing was published');
expect(out).toBe('object/lead: failed spec validation');
});
});
13 changes: 10 additions & 3 deletions packages/app-shell/src/views/studio-design/metadataError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from '@object-ui/data-objectstack';
import { resolveFlowTriggerKind } from '@objectstack/spec/automation';
import { DashboardWidgetSchema, resolveI18nLabel, type I18nLabel } from '@objectstack/spec/ui';
import type { RegisteredErrorCode } from '@objectstack/spec/api';
import type { NavTargetLabelResolver } from '@object-ui/layout';
import type { MetadataSelection } from '../metadata-admin/preview-registry.js';
import { t, tFormat, translateValidationMessage } from '../metadata-admin/i18n.js';
Expand All @@ -44,7 +45,7 @@ export interface PublishFailure {
type: string;
name: string;
error: string;
/** Machine code — `batch_aborted` marks a draft rolled back with the batch (ADR-0067 D2). */
/** Machine code — `BATCH_ABORTED` marks a draft rolled back with the batch (ADR-0067 D2). */
code?: string;
issues?: MetadataValidationIssue[];
}
Expand All @@ -53,8 +54,14 @@ export interface PublishFailure {
* framework 15.1+ (ADR-0067 D2): package publishes are ALL-OR-NOTHING. A
* failed batch reports every draft in `failed[]` — the causal item with its
* real error, the rest with this code — and `publishedCount: 0`.
*
* Spelled exactly as `publishPackageDrafts` emits it and as the spec's
* error-code ledger registers it, and compared exactly: ⛔ no case-folding
* (objectui#11985). The `satisfies` ties the literal to the ledger at compile
* time, so a code the spec does not register does not compile; the import is
* type-only and adds no runtime bytes.
*/
export const BATCH_ABORTED_CODE = 'batch_aborted';
export const BATCH_ABORTED_CODE = 'BATCH_ABORTED' satisfies RegisteredErrorCode;

/**
* Format the `failed[]` from a publish response (the server returns 200 with
Expand All @@ -63,7 +70,7 @@ export const BATCH_ABORTED_CODE = 'batch_aborted';
* Two server generations produce two shapes (both handled):
* - **15.1+ all-or-nothing** (ADR-0067 D2): the batch rolled back atomically —
* render ONE rolled-back banner anchored on the causal item(s), not N
* parallel errors (`batch_aborted` entries are consequences, not causes).
* parallel errors (`BATCH_ABORTED` entries are consequences, not causes).
* - **pre-15.1 partial publish**: each failed draft gets a heading and, when
* the failure was a validation error, its field-anchored issues indented
* below.
Expand Down
Loading