Skip to content

Commit 8e7c1da

Browse files
os-zhuangclaude
andauthored
fix(preview): draft-preview bar drops redundant Publish when nothing is pending (#1986)
Under the auto-publish posture an AI build leaves zero pending drafts, yet opening a draft preview still showed 'Nothing here is live until you publish' + 'Changes (0)' + a Publish button — a self-contradicting, no-op CTA. DraftPreviewBar now reflects the real pending-draft count: a known-zero count softens the bar to a neutral preview indicator and drops the Publish/Changes affordances; an unknown count keeps the publish path. Aligns with HomePage (count-gated) and RuntimeDraftBar (draft-gated). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 35487ea commit 8e7c1da

5 files changed

Lines changed: 143 additions & 22 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
fix(preview): draft-preview bar no longer demands a redundant Publish when nothing is pending
7+
8+
Under the auto-publish posture an AI build leaves zero pending drafts, yet opening a
9+
draft preview still showed "Draft preview — Nothing here is live until you publish."
10+
alongside "Changes (0)" and a Publish button — a self-contradicting, no-op call to
11+
action. `DraftPreviewBar` now reflects the real pending-draft count: when it is
12+
known to be zero the bar softens to a neutral preview indicator and drops the
13+
Publish/Changes affordances; an unknown count (still loading / fetch failed) keeps
14+
the publish path. `HomePage` (count-gated) and `RuntimeDraftBar` (draft-gated)
15+
already behaved this way — this aligns the third surface.

packages/app-shell/src/preview/DraftPreviewBar.tsx

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import { useEffect, useState } from 'react';
1818
import { useLocation, useNavigate } from 'react-router-dom';
1919
import { Eye, X, Rocket, GitCompareArrows } from 'lucide-react';
20-
import { Button } from '@object-ui/components';
20+
import { Button, cn } from '@object-ui/components';
2121
import { useObjectTranslation } from '@object-ui/i18n';
2222
import { usePreviewDrafts, markPreviewExit, PREVIEW_QUERY_FLAG } from './PreviewModeContext';
2323
import { usePublishAllDrafts } from './usePublishAllDrafts';
@@ -77,34 +77,55 @@ export function DraftPreviewBar() {
7777
setTimeout(() => { try { window.location.reload(); } catch { /* ignore */ } }, 300);
7878
};
7979

80+
// Under the auto-publish posture (and any time a draft preview is opened with
81+
// nothing staged) there are zero pending drafts. Claiming "nothing is live
82+
// until you publish" and offering a Publish button then is both false and a
83+
// no-op, so the bar drops the publish affordance and softens to a neutral
84+
// preview indicator. An UNKNOWN count (null — still loading or the fetch
85+
// failed) keeps the publish path: we only relax when we KNOW the count is zero.
86+
const noChanges = pendingCount === 0;
87+
8088
return (
8189
<div
82-
className="sticky top-0 z-40 flex items-center gap-3 border-b border-amber-300/70 bg-amber-50 px-4 py-2 text-sm text-amber-900 dark:border-amber-700/60 dark:bg-amber-950/40 dark:text-amber-200"
90+
className={cn(
91+
'sticky top-0 z-40 flex items-center gap-3 border-b px-4 py-2 text-sm',
92+
noChanges
93+
? 'border-slate-200 bg-slate-50 text-slate-700 dark:border-slate-700/60 dark:bg-slate-900/40 dark:text-slate-300'
94+
: 'border-amber-300/70 bg-amber-50 text-amber-900 dark:border-amber-700/60 dark:bg-amber-950/40 dark:text-amber-200',
95+
)}
8396
data-testid="draft-preview-bar"
8497
>
8598
<Eye className="h-4 w-4 shrink-0" />
8699
<p className="min-w-0 flex-1 truncate">
87-
{t('preview.draftBar.message', {
88-
defaultValue: 'Draft preview — you are seeing unpublished changes. Nothing here is live until you publish.',
89-
})}
100+
{noChanges
101+
? t('preview.draftBar.messageClean', {
102+
defaultValue: 'Draft preview — no unpublished changes; everything here is already live.',
103+
})
104+
: t('preview.draftBar.message', {
105+
defaultValue: 'Draft preview — you are seeing unpublished changes. Nothing here is live until you publish.',
106+
})}
90107
</p>
91-
<Button
92-
size="sm"
93-
variant="outline"
94-
onClick={() => setChangesOpen(true)}
95-
data-testid="draft-preview-changes"
96-
>
97-
<GitCompareArrows className="mr-1 h-3.5 w-3.5" />
98-
{t('preview.draftBar.changes', { defaultValue: 'Changes' })}
99-
{typeof pendingCount === 'number' ? ` (${pendingCount})` : ''}
100-
</Button>
101-
<Button size="sm" onClick={publish} disabled={publishing} data-testid="draft-preview-publish">
102-
<Rocket className="mr-1 h-3.5 w-3.5" />
103-
{publishing
104-
? t('preview.draftBar.publishing', { defaultValue: 'Publishing…' })
105-
: t('preview.draftBar.publish', { defaultValue: 'Publish' })}
106-
</Button>
107-
<DraftChangesPanel open={changesOpen} onOpenChange={setChangesOpen} />
108+
{!noChanges && (
109+
<>
110+
<Button
111+
size="sm"
112+
variant="outline"
113+
onClick={() => setChangesOpen(true)}
114+
data-testid="draft-preview-changes"
115+
>
116+
<GitCompareArrows className="mr-1 h-3.5 w-3.5" />
117+
{t('preview.draftBar.changes', { defaultValue: 'Changes' })}
118+
{typeof pendingCount === 'number' ? ` (${pendingCount})` : ''}
119+
</Button>
120+
<Button size="sm" onClick={publish} disabled={publishing} data-testid="draft-preview-publish">
121+
<Rocket className="mr-1 h-3.5 w-3.5" />
122+
{publishing
123+
? t('preview.draftBar.publishing', { defaultValue: 'Publishing…' })
124+
: t('preview.draftBar.publish', { defaultValue: 'Publish' })}
125+
</Button>
126+
<DraftChangesPanel open={changesOpen} onOpenChange={setChangesOpen} />
127+
</>
128+
)}
108129
<Button size="sm" variant="outline" onClick={exit} data-testid="draft-preview-exit">
109130
<X className="mr-1 h-3.5 w-3.5" />
110131
{t('preview.draftBar.exit', { defaultValue: 'Exit preview' })}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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+
import '@testing-library/jest-dom/vitest';
10+
import { afterEach, describe, expect, it, vi } from 'vitest';
11+
import { render, screen, waitFor } from '@testing-library/react';
12+
import { MemoryRouter } from 'react-router-dom';
13+
14+
// In preview mode regardless of the URL plumbing — we are testing the bar's
15+
// own count logic, not the query-flag reader.
16+
vi.mock('../PreviewModeContext', () => ({
17+
usePreviewDrafts: () => true,
18+
markPreviewExit: vi.fn(),
19+
PREVIEW_QUERY_FLAG: 'preview',
20+
}));
21+
vi.mock('@object-ui/i18n', () => ({
22+
useObjectTranslation: () => ({
23+
t: (_k: string, o?: { defaultValue?: string }) => o?.defaultValue ?? _k,
24+
}),
25+
}));
26+
vi.mock('../usePublishAllDrafts', () => ({
27+
usePublishAllDrafts: () => ({ publishAll: vi.fn(async () => ({ ok: true })), publishing: false }),
28+
}));
29+
vi.mock('../DraftChangesPanel', () => ({ DraftChangesPanel: () => null }));
30+
31+
import { DraftPreviewBar } from '../DraftPreviewBar';
32+
33+
function renderBar() {
34+
return render(
35+
<MemoryRouter initialEntries={['/app/x?preview=draft']}>
36+
<DraftPreviewBar />
37+
</MemoryRouter>,
38+
);
39+
}
40+
41+
function mockDrafts(list: unknown) {
42+
global.fetch = vi.fn(async () => ({ ok: true, json: async () => list })) as unknown as typeof fetch;
43+
}
44+
45+
afterEach(() => {
46+
vi.restoreAllMocks();
47+
});
48+
49+
describe('DraftPreviewBar', () => {
50+
it('drops the redundant Publish affordance when there are zero pending drafts (auto-publish norm)', async () => {
51+
mockDrafts([]);
52+
renderBar();
53+
// The bar is always the preview-mode indicator…
54+
expect(screen.getByTestId('draft-preview-bar')).toBeInTheDocument();
55+
// …but once we KNOW the count is zero, Publish + Changes go away and the
56+
// copy stops claiming nothing is live.
57+
await waitFor(() => {
58+
expect(screen.queryByTestId('draft-preview-publish')).not.toBeInTheDocument();
59+
});
60+
expect(screen.queryByTestId('draft-preview-changes')).not.toBeInTheDocument();
61+
expect(screen.getByTestId('draft-preview-bar')).toHaveTextContent(/no unpublished changes/i);
62+
expect(screen.getByTestId('draft-preview-exit')).toBeInTheDocument();
63+
});
64+
65+
it('keeps Publish + the warning message + Changes(N) when drafts are genuinely pending', async () => {
66+
mockDrafts([{ type: 'object', name: 'a' }, { type: 'object', name: 'b' }]);
67+
renderBar();
68+
await waitFor(() => {
69+
expect(screen.getByTestId('draft-preview-publish')).toBeInTheDocument();
70+
});
71+
expect(screen.getByTestId('draft-preview-changes')).toHaveTextContent('(2)');
72+
expect(screen.getByTestId('draft-preview-bar')).toHaveTextContent(/Nothing here is live until you publish/i);
73+
});
74+
75+
it('keeps Publish visible when the draft count is unknown (fetch failed) — only a known zero relaxes', async () => {
76+
global.fetch = vi.fn(async () => ({ ok: false, json: async () => null })) as unknown as typeof fetch;
77+
renderBar();
78+
// pendingCount stays null → not known-zero → safe default keeps the publish path.
79+
await waitFor(() => {
80+
expect(screen.getByTestId('draft-preview-publish')).toBeInTheDocument();
81+
});
82+
});
83+
});

packages/i18n/src/locales/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1799,6 +1799,7 @@ const en = {
17991799
},
18001800
draftBar: {
18011801
message: 'Draft preview — you are seeing unpublished changes. Nothing here is live until you publish.',
1802+
messageClean: 'Draft preview — no unpublished changes; everything here is already live.',
18021803
publish: 'Publish',
18031804
publishing: 'Publishing…',
18041805
exit: 'Exit preview',

packages/i18n/src/locales/zh.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1804,6 +1804,7 @@ const zh = {
18041804
},
18051805
draftBar: {
18061806
message: '草稿预览 —— 您看到的是未发布的更改,发布前不会生效。',
1807+
messageClean: '草稿预览 —— 没有未发布的更改,此处内容均已生效。',
18071808
publish: '发布',
18081809
publishing: '发布中…',
18091810
exit: '退出预览',

0 commit comments

Comments
 (0)