Skip to content

Commit 67dcfcb

Browse files
committed
fix(app-shell,i18n): the Organization flows page states how its drafts publish, and a deep-link miss shows no false "no designers" notice (objectui#11591)
The package-less sheet's confirm note reads a new `preview.changes.confirmNoteSeparate` family (all ten packs): each draft publishes by itself and one that fails stays pending. A package's sheet keeps `preview.changes.confirmNote` unchanged. The Automations pillar reads the preview registry for `flow`, its one type, instead of the open flow's type, so with no flow open the canvas chip no longer says no designers are registered. Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2e818d0 commit 67dcfcb

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)