-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathUnpublishedAppBar.tsx
More file actions
97 lines (91 loc) · 3.98 KB
/
Copy pathUnpublishedAppBar.tsx
File metadata and controls
97 lines (91 loc) · 3.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* ADR-0045 — the "unpublished app" banner. Renders while the CURRENT app is
* materialized-but-unlisted (`app.hidden === true`): the app is fully real —
* tables, data, interactions — but end users can't see it (launchers exclude
* it; the REST gate strips it for non-builders). The banner narrates that
* state and offers the one action that matters: Publish, which simply flips
* visibility (`hidden: false`) — instant and reversible, per ADR-0045.
*
* Sibling of DraftPreviewBar (the ADR-0037 draft-overlay watermark): that bar
* owns mutation preview (`?preview=draft`); this one owns the materialize
* regime. In preview mode this bar yields — the draft bar already narrates.
*/
import { useState } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { EyeOff, Rocket } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@object-ui/components';
import { useObjectTranslation } from '@object-ui/i18n';
import { useMetadata } from '../providers/MetadataProvider';
import { usePreviewDrafts } from './PreviewModeContext';
export function UnpublishedAppBar() {
const preview = usePreviewDrafts();
const { appName } = useParams();
const location = useLocation();
const { apps, refresh } = useMetadata();
const { t } = useObjectTranslation();
const [publishing, setPublishing] = useState(false);
// The draft-preview watermark owns the preview tree; never stack both bars.
if (preview) return null;
const routeApp = appName ?? location.pathname.match(/\/apps\/([^/?#]+)/)?.[1];
if (!routeApp) return null;
const app = (apps ?? []).find((a: any) => a?.name === routeApp);
if (!app || (app as any).hidden !== true) return null;
const publish = async () => {
setPublishing(true);
try {
// Publish = the ADR-0045 visibility flip: one metadata write, no
// lifecycle machinery. Body is the full current app with hidden:false
// (the meta save endpoint replaces the overlay row).
const res = await fetch(`/api/v1/meta/app/${encodeURIComponent(routeApp)}`, {
method: 'PUT',
credentials: 'include',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ ...(app as Record<string, unknown>), hidden: false }),
});
if (!res.ok) {
const payload = await res.json().catch(() => null);
throw new Error((payload as any)?.error?.message ?? (payload as any)?.error ?? `HTTP ${res.status}`);
}
toast.success(
t('preview.unpublishedBar.published', {
defaultValue: 'Published! The app is now visible to your users.',
}),
);
refresh?.();
} catch (e) {
toast.error(
`${t('preview.unpublishedBar.publishFailed', { defaultValue: 'Publish failed' })}: ${(e as Error).message}`,
);
} finally {
setPublishing(false);
}
};
return (
<div
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"
data-testid="unpublished-app-bar"
>
<EyeOff className="h-4 w-4 shrink-0" />
<p className="min-w-0 flex-1 truncate">
{t('preview.unpublishedBar.message', {
defaultValue:
'Unpublished app — fully functional, but only builders can see it. Publish to make it visible to your users.',
})}
</p>
<Button size="sm" onClick={publish} disabled={publishing} data-testid="unpublished-app-publish">
<Rocket className="mr-1 h-3.5 w-3.5" />
{publishing
? t('preview.unpublishedBar.publishing', { defaultValue: 'Publishing…' })
: t('preview.unpublishedBar.publish', { defaultValue: 'Publish' })}
</Button>
</div>
);
}