Skip to content

Commit b92329c

Browse files
fix(app-shell,i18n): the Organization flows page says each draft publishes by itself, and a deep-link miss shows no false "no designers" notice (objectui#11591) (#11606)
Fixes #11591 Clause-②: no Implemented by an os-dev run of the PM claim on the card (branch `claude/issue-11591-org-flows-copy`, session `https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2`). ## What changed ### 1. The package-less sheet no longer claims atomicity - `DraftChangesPanel`'s confirm note reads `preview.changes.confirmNote` when the panel has a `packageId` (unchanged, byte for byte, in all ten packs) and the new `preview.changes.confirmNoteSeparate` when it has none. - Why `packageId` decides: only a package has a batch door (`POST /packages/:id/publish-drafts`). The package-less page's Publish (the `packageId === null` branch of `doPublish` in `StudioDesignSurface`) calls `publishDraft` once per draft and keeps going past a failure, so a partial publish is possible there. - The new family is in all ten packs, with each pack's own CLDR categories plus the base key and the en parity slots: en/de one+other, es/fr/pt one+many+other, ru one+few+many+other, ar zero through other, ja/ko/zh other. en reads: - one: "Publishing releases the 1 pending draft on its own: if it fails, it stays pending." - other: "Publishing releases the 2 pending drafts one at a time: a draft that fails stays pending while the others go live." - The panel's other host, the draft-preview bar, passes neither `packageId` nor `onPublish`. It renders no confirm footer and does not change. ### 2. A deep-link miss and an empty list show no false "no designers" notice - `AutomationsPillar` read `getMetadataPreview(current?.type ?? '')`. With no flow open, that read is for the type `''`, which returns undefined. `isEditable` was then false, and the canvas chip rendered `engine.studio.auto.designersMissing` ("No metadata designers are registered in this session…") next to the correct miss sentence. - It now reads `getMetadataPreview('flow')`, the pillar's one type, the same way the `getMetadataInspector('flow')` read beside it already worked. Nothing changes with a flow open, because every rail entry of this pillar has the type `flow`. - The notice still keys on the registry: where no designer is registered, it still shows with no flow open (pinned below). - `views/metadata-admin/**` is not edited (objectui#10202 holds it). The copy keys already existed. ## Measured All at `67dcfcb` unless said otherwise. **Red before the fix** (tests written first, run on the unfixed tree): 15 failed, 41 passed across the 4 pin files. The received strings were: - package-less sheet, 1 draft: "Publishing releases the 1 pending draft of this package atomically." - package-less sheet, 2 drafts: "Publishing releases all 2 pending drafts of this package atomically." - `/studio/~org/automations?surface=flow:showcase_urgent_task_alert` (the card's measured URL): the page text held "No metadata designers are registered in this session, so this flow cannot be designed here." beside "The link names flow “showcase_urgent_task_alert”, which is not here." - The same on a package page's miss. - An empty package-less list: the measurement the card predicted. The rail's "No flows outside a package yet. …" sat beside the same false notice. - ten packs: no `confirmNoteSeparate` family. **Green after the fix**: `pnpm exec vitest run` on the 4 pin files gave 4 files, 56 of 56 tests passed. **Ablations.** Each one went through `ablation-replace.mjs`, which checks that the anchor hit once and that the blob changed. Each was restored by blob hash with `git diff HEAD` empty: - The atomic sentence back on the package-less path (the note's condition forced to `true`): 2 failed, 2 passed. The two package-less cases failed and received the atomic sentence. The two package-sheet controls stayed green. - The notice keyed on the selection again (`getMetadataPreview(current?.type ?? '')`): 3 failed, 18 passed. The failures were the measured miss, the package page's miss and the empty list. The control with a flow open stayed green. So did the empty-registry case, because with no designer registered the notice shows either way, which is the direction expected. - The ja package-less form given the package sentence's "まとめて(アトミックに)": 1 failed, 30 passed. Only ja failed. **Wider runs.** - `packages/app-shell/src/views/studio-design/` and `packages/app-shell/src/preview/`: 103 files, 653 tests passed. - `packages/i18n/`, plus every test that reads a changed file as source text: 117 files, 2663 passed and 13 skipped. These tests were found by grepping test sources for `readFileSync` and `?raw`, and for the names `StudioDesignSurface.tsx`, `DraftChangesPanel.tsx` and `locales/*.ts`. They are `packages/app-shell/src/__tests__/`, `ObjectView.viewConfigGate`, the metadata-admin i18n tests, `HomeItemTypeLabel.parity`, `apiErrorEnvelope` and the `scripts/__tests__` suites for the i18n gates, node-esm-load, eager-closure, eager-locale-catalogues, spec-symbol ledger and derivation, one-authority, vi-mock-inherit, side-effects, self-import and turbo build outputs. **Type-check.** `pnpm --filter @object-ui/app-shell type-check` and `pnpm --filter @object-ui/i18n type-check` both exited 0. They ran after `pnpm turbo run build --filter='@object-ui/app-shell^...'` (28 of 28 tasks). Both test tsconfigs list the new test files; this was checked with `--listFilesOnly`. **Gates, all exit 0.** The gates run were: - the i18n gates: `check:i18n-keys`, `check:i18n-drift` ("No en value changed in this range"), `check:i18n-dead-keys` (the new keys are not candidates) and `check:i18n-designer-parity`; - the named file gates: `check:control-bytes`, `check:new-line-citations` (0 new) and `check:changeset-claims`; - the test and mock gates: `check:pending-changeset-literals`, `check:test-path-roots`, `check:vi-mock-inherit`, `check:vi-mock-override-shape` and `check:vi-mock-specifiers`; - the source gates: `check:spec-symbols`, `check:metadata-write-doors`, `check:handler-key-reads`, `check:esm-specifiers`, `check:unreferenced-sources`, `check:self-import` and `check:phantom-deps`; - the changeset gates: `scripts/check-changeset-presence.mjs` and `changeset:check`. **Lint (narrowed, not the repo-wide run).** - `eslint --format json` ran on the 16 changed `.ts`/`.tsx` files; the JSON output has 16 results. It reported 0 errors. - The two changed sources carry 2 and 17 warnings. The rule multisets are identical to those of the same two files at the base `2e818d0`. The base files were linted in place and then restored by blob hash. - `eslint.config.js` sets no `parserOptions.project` and no `projectService`, so linting is not type-aware. This diff therefore cannot move a verdict in a file it does not touch. **NOT MEASURED** (left to CI): - `check:eager-closure` / `check:eager-locale-catalogues`: they need a console build. The en pack is in the eager closure, and this adds three en strings. - `pnpm test` (all 8 shards). - `pnpm lint`. ## Acceptance notes - With no flow open, the pillar's other text is unchanged. The chip now reads its standing "Visual orchestration · click a node to configure". The canvas reads "Select an automation" on an empty list. The configuration panel reads "Click a node on the canvas, and its configuration appears here." The card's pin is the false notice. These are observations, and nothing was filed for them. - In en, de, es, fr and pt, the single-draft form does not mention "the others", because a partial publish needs at least two drafts. ru's `one` category also covers 21, 31 and so on, so its one-form avoids "the others" too. ar, ja, ko and zh use one sentence for every count, as their `confirmNote` does. - Changeset `.changeset/11591-org-flows-copy.md`: `@object-ui/app-shell` patch, `@object-ui/i18n` patch. --- _Generated by [Claude Code](https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 902ebab commit b92329c

17 files changed

Lines changed: 417 additions & 8 deletions

‎.changeset/11591-org-flows-copy.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
Studio's "Organization flows" page no longer says its drafts publish atomically, and a deep link to a flow that is not on the page no longer says no metadata designers are registered (objectui#11591).
7+
8+
On the package-less page (`/studio/~org/automations`), the pending-changes sheet read "Publishing releases the 1 pending draft of this package atomically." That page has no package, and its Publish promotes each draft by itself: a draft that fails stays pending while the others go live. The sheet now says so there, through a new `preview.changes.confirmNoteSeparate` plural family in all ten language packs. A package's sheet keeps its sentence unchanged. `DraftChangesPanel` picks the sentence from its `packageId` prop: with a package, the atomic sentence; without one, the per-draft sentence.
9+
10+
On the Automations pillar, the canvas chip read the designer registry for the open flow's type. With no flow open (a deep link naming a flow the list does not hold, or an empty list), it found none and showed "No metadata designers are registered in this session…" beside the right message, on a page whose flow designer is registered. The chip now reads the registry for the pillar's own type, `flow`, as the configuration panel beside it already did. The notice still shows when no designer is registered, whether or not a flow is open.

‎packages/app-shell/src/preview/DraftChangesPanel.tsx‎

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,12 @@ function EntryDetail({ entry }: { entry: DraftChangeEntry }) {
335335
export interface DraftChangesPanelProps {
336336
open: boolean;
337337
onOpenChange: (open: boolean) => void;
338-
/** When set, list only pending drafts belonging to this package (Studio is package-scoped). */
338+
/**
339+
* When set, list only pending drafts belonging to this package (Studio is
340+
* package-scoped), and the confirm note says they publish as one atomic
341+
* batch. Without one, the note says each draft publishes by itself
342+
* (objectui#11591).
343+
*/
339344
packageId?: string | null;
340345
/**
341346
* Narrows the listed drafts further, for a host whose scope the `_drafts`
@@ -627,11 +632,22 @@ export function DraftChangesPanel({
627632
{onPublish && (entries?.length ?? 0) > 0 && !error && (
628633
<div className="mt-auto flex flex-col gap-2 border-t px-4 py-3">
629634
<p className="text-xs text-muted-foreground">
630-
{t('preview.changes.confirmNote', {
631-
count: entries!.length,
632-
defaultValue:
633-
'Publishing releases all {{count}} pending drafts of this package atomically.',
634-
})}
635+
{/* objectui#11591 — the note states what THIS scope's Publish
636+
does. Only a package publishes as one atomic batch (`POST
637+
/packages/:id/publish-drafts`); with no package there is no
638+
batch door, so the caller promotes each draft by itself and a
639+
draft that fails stays pending while the others go live. */}
640+
{packageId
641+
? t('preview.changes.confirmNote', {
642+
count: entries!.length,
643+
defaultValue:
644+
'Publishing releases all {{count}} pending drafts of this package atomically.',
645+
})
646+
: t('preview.changes.confirmNoteSeparate', {
647+
count: entries!.length,
648+
defaultValue:
649+
'Publishing releases the {{count}} pending drafts one at a time: a draft that fails stays pending while the others go live.',
650+
})}
635651
</p>
636652
<Button
637653
size="sm"
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* objectui#11591 — the pending-changes sheet's confirm note states what THAT
11+
* scope's Publish does.
12+
*
13+
* A package's sheet publishes through `POST /packages/:id/publish-drafts`, one
14+
* atomic pass, and its note says so. The package-less "Organization flows"
15+
* page (objectui#11553) has no package to publish as a batch: its Publish
16+
* promotes each draft by reference, one request per draft, and a draft that
17+
* fails stays pending while the others go live. The sheet there used to read
18+
* "Publishing releases the 1 pending draft of this package atomically." — a
19+
* package it does not have, and an atomicity it does not deliver.
20+
*
21+
* Rendered through a REAL `I18nProvider` over the shipped `en` pack, so the
22+
* sentence asserted is the one an author reads, not the call site's inline
23+
* `defaultValue`. That every locale's copy moved with `en` is pinned beside
24+
* the packs, in `packages/i18n/src/__tests__/confirmNoteSeparate-11591.test.ts`.
25+
*/
26+
27+
import '@testing-library/jest-dom/vitest';
28+
import * as React from 'react';
29+
import { afterEach, describe, expect, it, vi } from 'vitest';
30+
import { cleanup, render, screen, waitFor } from '@testing-library/react';
31+
import { createI18n, I18nProvider } from '@object-ui/i18n';
32+
33+
import { DraftChangesPanel } from '../DraftChangesPanel';
34+
35+
afterEach(() => {
36+
cleanup();
37+
vi.restoreAllMocks();
38+
});
39+
40+
type Draft = { type: string; name: string; packageId: string | null };
41+
42+
/** Answer the `_drafts` feed with `drafts`, and every other read as absent. */
43+
function serveDrafts(drafts: Draft[]) {
44+
global.fetch = vi.fn(async (input: RequestInfo | URL) => {
45+
const url = String(input);
46+
const ok = (body: unknown) => ({ ok: true, status: 200, json: async () => body });
47+
if (url.includes('/_drafts')) return ok(drafts);
48+
if (/\/meta\/flow(\?|$)/.test(url)) return ok([]);
49+
return { ok: false, status: 404, json: async () => ({}) };
50+
}) as unknown as typeof fetch;
51+
}
52+
53+
function renderSheet(packageId: string | null) {
54+
return render(
55+
<I18nProvider
56+
instance={createI18n({ defaultLanguage: 'en', detectBrowserLanguage: false })}
57+
persistLanguage={false}
58+
>
59+
<DraftChangesPanel open onOpenChange={() => {}} packageId={packageId} onPublish={vi.fn()} />
60+
</I18nProvider>,
61+
);
62+
}
63+
64+
/** The confirm footer's note — the paragraph right above the publish button. */
65+
async function confirmNote(): Promise<string> {
66+
const button = await screen.findByTestId('draft-changes-publish', undefined, { timeout: 4000 });
67+
const note = button.parentElement?.querySelector('p');
68+
if (!note) throw new Error('the confirm footer rendered no note');
69+
return note.textContent ?? '';
70+
}
71+
72+
describe('the confirm note says what this scope publishes (objectui#11591)', () => {
73+
it('package-less, one draft: no package, no atomicity', async () => {
74+
serveDrafts([{ type: 'flow', name: 'qa_urgent_alert_clone', packageId: null }]);
75+
renderSheet(null);
76+
77+
const note = await confirmNote();
78+
expect(note).toBe('Publishing releases the 1 pending draft on its own: if it fails, it stays pending.');
79+
expect(note).not.toMatch(/atomic/i);
80+
expect(note).not.toMatch(/package/i);
81+
});
82+
83+
it('package-less, several drafts: each separately, and a partial publish is possible', async () => {
84+
serveDrafts([
85+
{ type: 'flow', name: 'qa_urgent_alert_clone', packageId: null },
86+
{ type: 'flow', name: 'qa_daily_digest_clone', packageId: null },
87+
]);
88+
renderSheet(null);
89+
90+
const note = await confirmNote();
91+
expect(note).toBe(
92+
'Publishing releases the 2 pending drafts one at a time: a draft that fails stays pending while the others go live.',
93+
);
94+
expect(note).not.toMatch(/atomic/i);
95+
expect(note).not.toMatch(/package/i);
96+
});
97+
98+
it('control — a package’s sheet keeps its sentence, byte for byte', async () => {
99+
serveDrafts([{ type: 'flow', name: 'showcase_daily_digest', packageId: 'com.example.showcase' }]);
100+
renderSheet('com.example.showcase');
101+
102+
await waitFor(async () =>
103+
expect(await confirmNote()).toBe('Publishing releases the 1 pending draft of this package atomically.'),
104+
);
105+
});
106+
107+
it('control — a package’s sheet, several drafts', async () => {
108+
serveDrafts([
109+
{ type: 'flow', name: 'showcase_daily_digest', packageId: 'com.example.showcase' },
110+
{ type: 'flow', name: 'showcase_urgent_task_alert', packageId: 'com.example.showcase' },
111+
]);
112+
renderSheet('com.example.showcase');
113+
114+
expect(await confirmNote()).toBe('Publishing releases all 2 pending drafts of this package atomically.');
115+
});
116+
});

‎packages/app-shell/src/views/studio-design/StudioDesignSurface.designerRegistryMissing.test.tsx‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,4 +314,30 @@ describe('Automations pillar — the fourth site, found by sweeping (#6795 C, si
314314
expect(document.body.textContent ?? '').not.toContain('click a node to configure');
315315
expect(document.body.textContent ?? '').not.toContain('Click a node on the canvas');
316316
});
317+
318+
/**
319+
* objectui#11591 — the notice keys on the REGISTRY, not on an open flow: with
320+
* no flow open (a deep link naming one the rail does not hold) it still
321+
* shows here, where the registry is empty, and it does not show where the
322+
* registry is populated — that half is pinned in
323+
* `StudioDesignSurface.packageLessFlows-11553.test.tsx`.
324+
*/
325+
it('with no flow open, still names the reason — the registry, not the selection, decides', async () => {
326+
assertRegistriesEmptyWithControl();
327+
mockClient.layered.mockClear();
328+
329+
render(
330+
<MemoryRouter initialEntries={['/studio/~org/automations?surface=flow%3Anobody_has_this']}>
331+
<AutomationsPillar packageId={null} />
332+
</MemoryRouter>,
333+
);
334+
335+
await screen.findByText('The link names flow “nobody_has_this”, which is not here.', undefined, { timeout: 4000 });
336+
const hits = screen.getAllByText(
337+
'No metadata designers are registered in this session, so this flow cannot be designed here.',
338+
);
339+
// The canvas header chip AND the rail, as with a flow open.
340+
expect(hits.length).toBe(2);
341+
expect(mockClient.layered).not.toHaveBeenCalledWith('flow', expect.anything());
342+
});
317343
});

‎packages/app-shell/src/views/studio-design/StudioDesignSurface.packageLessFlows-11553.test.tsx‎

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn(), info: vi.f
132132

133133
import { StudioDesignSurface } from './StudioDesignSurface';
134134
import { createEmptyDataSource, failOnAbsorbedFetchError } from './__tests__/emptyDataSource';
135-
import { registerMetadataPreview } from '../metadata-admin/preview-registry';
135+
import { getMetadataPreview, registerMetadataPreview } from '../metadata-admin/preview-registry';
136136
import { registerMetadataInspector } from '../metadata-admin/inspector-registry';
137137
import { FlowPreview } from '../metadata-admin/previews/FlowPreview';
138138
import { FlowInspector } from '../metadata-admin/inspectors/FlowInspector';
@@ -379,3 +379,65 @@ describe('the package-less scope reviews and publishes exactly its own drafts (o
379379
expect(server.packageBatches).toEqual([]);
380380
});
381381
});
382+
383+
/**
384+
* objectui#11591 — the "no designers" notice keys on the designer registry,
385+
* never on the absence of an open flow. On a deep-link miss and on an empty
386+
* list no flow is open, and the canvas chip used to read the registry for the
387+
* OPEN flow's type — none — so it said "No metadata designers are registered
388+
* in this session…" on a page whose designers are registered (this file
389+
* registers `FlowPreview` / `FlowInspector` above, and the controls below read
390+
* the registry back). That the notice still shows when the registry really is
391+
* empty, open flow or none, is pinned beside its other sites, in
392+
* `StudioDesignSurface.designerRegistryMissing.test.tsx`.
393+
*/
394+
describe('a deep-link miss and an empty list show no "no designers" notice (objectui#11591)', () => {
395+
const NO_DESIGNERS = 'No metadata designers are registered';
396+
397+
it('the measured miss: the package-less page, a link naming a packaged flow', async () => {
398+
renderAt('/studio/~org/automations?surface=flow%3Ashowcase_urgent_task_alert');
399+
400+
await screen.findByText('The link names flow “showcase_urgent_task_alert”, which is not here.', undefined, {
401+
timeout: 8000,
402+
});
403+
// Control: the designers are registered, so the notice would be false.
404+
expect(getMetadataPreview('flow')).toBe(FlowPreview);
405+
expect(document.body.textContent).not.toContain(NO_DESIGNERS);
406+
expect(screen.getByText('Visual orchestration · click a node to configure')).toBeInTheDocument();
407+
// No flow was opened in the missing one's place.
408+
expect(mockClient.layered).not.toHaveBeenCalled();
409+
expect(screen.getByRole('button', { name: /QA urgent alert clone/ })).toBeInTheDocument();
410+
});
411+
412+
it('a package page’s miss, too', async () => {
413+
renderAt(`/studio/${SHOWCASE}/automations?surface=flow%3Anobody_has_this`);
414+
415+
await screen.findByText('The link names flow “nobody_has_this”, which is not here.', undefined, { timeout: 8000 });
416+
expect(getMetadataPreview('flow')).toBe(FlowPreview);
417+
expect(document.body.textContent).not.toContain(NO_DESIGNERS);
418+
});
419+
420+
it('an empty list shows the rail’s own empty state — no miss, no "no designers" notice', async () => {
421+
server.active.delete('flow/qa_urgent_alert_clone');
422+
renderAt('/studio/~org/automations');
423+
424+
await screen.findByText(
425+
'No flows outside a package yet. Clone a packaged flow in Setup › Packaged automation to edit the copy here.',
426+
undefined,
427+
{ timeout: 8000 },
428+
);
429+
expect(screen.getByText('Select an automation')).toBeInTheDocument();
430+
expect(getMetadataPreview('flow')).toBe(FlowPreview);
431+
expect(document.body.textContent).not.toContain(NO_DESIGNERS);
432+
expect(document.body.textContent).not.toContain('The link names flow');
433+
expect(mockClient.layered).not.toHaveBeenCalled();
434+
});
435+
436+
it('control: an open flow on the same page reads the same chip', async () => {
437+
renderAt('/studio/~org/automations');
438+
await openedFlow('qa_urgent_alert_clone');
439+
440+
expect(screen.getByText('Visual orchestration · click a node to configure')).toBeInTheDocument();
441+
expect(document.body.textContent).not.toContain(NO_DESIGNERS);
442+
});
443+
});

‎packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4584,7 +4584,12 @@ export function AutomationsPillar({
45844584
// offer a way to author the first one (mirrors the object/app creators).
45854585
const [creating, setCreating] = React.useState(false);
45864586
const [createBusy, setCreateBusy] = React.useState(false);
4587-
const Preview = getMetadataPreview(current?.type ?? '');
4587+
// objectui#11591 — keyed on the pillar's one type, as the inspector beside
4588+
// it is, never on the open flow's: with no flow open (a deep link naming one
4589+
// this rail does not hold, or an empty rail) a selection-keyed read found no
4590+
// designer and the canvas chip below said none were registered, on a page
4591+
// whose designers are.
4592+
const Preview = getMetadataPreview('flow');
45884593
const inspector = getMetadataInspector('flow');
45894594
const isEditable = !!Preview;
45904595
// objectui#6795 part C — the FOURTH site, found by sweeping past the three the

0 commit comments

Comments
 (0)