@object-ui/app-shell 6.0.3 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/dist/console/marketplace/MarketplaceAccessDenied.d.ts +9 -0
- package/dist/console/marketplace/MarketplaceAccessDenied.js +18 -0
- package/dist/console/marketplace/MarketplaceInstalledPage.js +5 -0
- package/dist/console/marketplace/MarketplacePackagePage.js +35 -2
- package/dist/console/marketplace/MarketplacePage.js +5 -0
- package/dist/views/RecordDetailView.js +12 -0
- package/package.json +28 -28
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# @object-ui/app-shell — Changelog
|
|
2
2
|
|
|
3
|
+
## 6.1.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [991b62d]
|
|
8
|
+
- @object-ui/core@6.1.0
|
|
9
|
+
- @object-ui/types@6.1.0
|
|
10
|
+
- @object-ui/components@6.1.0
|
|
11
|
+
- @object-ui/data-objectstack@6.1.0
|
|
12
|
+
- @object-ui/fields@6.1.0
|
|
13
|
+
- @object-ui/layout@6.1.0
|
|
14
|
+
- @object-ui/react@6.1.0
|
|
15
|
+
- @object-ui/auth@6.1.0
|
|
16
|
+
- @object-ui/collaboration@6.1.0
|
|
17
|
+
- @object-ui/permissions@6.1.0
|
|
18
|
+
- @object-ui/providers@6.1.0
|
|
19
|
+
- @object-ui/i18n@6.1.0
|
|
20
|
+
|
|
21
|
+
## 6.0.4
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- 76e73fe: Gate App Marketplace pages by `useIsWorkspaceAdmin()`. Non-admin members of
|
|
26
|
+
the active organization can no longer load the marketplace catalog, package
|
|
27
|
+
detail, or installed-apps pages — they get an "admin-only" empty state
|
|
28
|
+
instead. The marketplace nav link in the sidebar was already gated; this
|
|
29
|
+
closes the direct-URL gap.
|
|
30
|
+
- @object-ui/types@6.0.4
|
|
31
|
+
- @object-ui/core@6.0.4
|
|
32
|
+
- @object-ui/i18n@6.0.4
|
|
33
|
+
- @object-ui/react@6.0.4
|
|
34
|
+
- @object-ui/components@6.0.4
|
|
35
|
+
- @object-ui/fields@6.0.4
|
|
36
|
+
- @object-ui/layout@6.0.4
|
|
37
|
+
- @object-ui/data-objectstack@6.0.4
|
|
38
|
+
- @object-ui/auth@6.0.4
|
|
39
|
+
- @object-ui/permissions@6.0.4
|
|
40
|
+
- @object-ui/collaboration@6.0.4
|
|
41
|
+
- @object-ui/providers@6.0.4
|
|
42
|
+
|
|
3
43
|
## 6.0.3
|
|
4
44
|
|
|
5
45
|
### Patch Changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marketplace access guard.
|
|
3
|
+
*
|
|
4
|
+
* App Marketplace install actions require owner/admin on the active
|
|
5
|
+
* organization. Non-admin members shouldn't see the catalog at all —
|
|
6
|
+
* there's nothing they can do with it. This component renders a
|
|
7
|
+
* minimal "no access" empty state for the marketplace surface.
|
|
8
|
+
*/
|
|
9
|
+
export declare function MarketplaceAccessDenied(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Marketplace access guard.
|
|
4
|
+
*
|
|
5
|
+
* App Marketplace install actions require owner/admin on the active
|
|
6
|
+
* organization. Non-admin members shouldn't see the catalog at all —
|
|
7
|
+
* there's nothing they can do with it. This component renders a
|
|
8
|
+
* minimal "no access" empty state for the marketplace surface.
|
|
9
|
+
*/
|
|
10
|
+
import { Card, CardContent, Button } from '@object-ui/components';
|
|
11
|
+
import { Lock } from 'lucide-react';
|
|
12
|
+
import { useNavigate, useParams } from 'react-router-dom';
|
|
13
|
+
export function MarketplaceAccessDenied() {
|
|
14
|
+
const navigate = useNavigate();
|
|
15
|
+
const { appName } = useParams();
|
|
16
|
+
const home = appName ? `/apps/${appName}` : '/';
|
|
17
|
+
return (_jsx("div", { className: "container mx-auto max-w-2xl px-6 py-16", children: _jsx(Card, { children: _jsxs(CardContent, { className: "flex flex-col items-center gap-4 py-12 text-center", children: [_jsx("div", { className: "rounded-full bg-muted p-3", children: _jsx(Lock, { className: "h-6 w-6 text-muted-foreground" }) }), _jsxs("div", { children: [_jsx("h2", { className: "text-lg font-semibold", children: "App Marketplace is admin-only" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "You don't have permission to install apps in this environment. Ask an owner or admin of this organization for access." })] }), _jsx(Button, { variant: "outline", onClick: () => navigate(home), children: "Back to home" })] }) }) }));
|
|
18
|
+
}
|
|
@@ -19,10 +19,13 @@ import { useEffect, useState } from 'react';
|
|
|
19
19
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
20
20
|
import { Card, CardContent, CardHeader, CardTitle, Badge, Button, Skeleton, } from '@object-ui/components';
|
|
21
21
|
import { ArrowLeft, RefreshCcw, Store, Trash2, AlertCircle, ExternalLink } from 'lucide-react';
|
|
22
|
+
import { useIsWorkspaceAdmin } from '@object-ui/auth';
|
|
22
23
|
import { listLocalInstalls, uninstallLocal, } from './marketplaceApi';
|
|
24
|
+
import { MarketplaceAccessDenied } from './MarketplaceAccessDenied';
|
|
23
25
|
export function MarketplaceInstalledPage() {
|
|
24
26
|
const navigate = useNavigate();
|
|
25
27
|
const { appName } = useParams();
|
|
28
|
+
const isAdmin = useIsWorkspaceAdmin();
|
|
26
29
|
const basePath = appName ? `/apps/${appName}` : '';
|
|
27
30
|
const [items, setItems] = useState([]);
|
|
28
31
|
const [loading, setLoading] = useState(true);
|
|
@@ -60,5 +63,7 @@ export function MarketplaceInstalledPage() {
|
|
|
60
63
|
setWorking(null);
|
|
61
64
|
}
|
|
62
65
|
};
|
|
66
|
+
if (!isAdmin)
|
|
67
|
+
return _jsx(MarketplaceAccessDenied, {});
|
|
63
68
|
return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6 max-w-5xl", children: [_jsxs(Button, { variant: "ghost", size: "sm", className: "self-start", onClick: () => navigate(`${basePath}/system/marketplace`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Back to marketplace"] }), _jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Store, { className: "h-6 w-6 text-primary", "aria-hidden": "true" }), _jsx("h1", { className: "text-xl sm:text-2xl font-bold tracking-tight", children: "Installed Apps" })] }), _jsxs("p", { className: "text-sm text-muted-foreground mt-1", children: ["Marketplace packages currently installed into this runtime's kernel. Cached manifests live in ", _jsx("code", { className: "font-mono text-xs", children: ".objectstack/installed-packages/" }), " and survive restarts."] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void load(), disabled: loading, children: [_jsx(RefreshCcw, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Refresh"] })] }), result && (_jsx("div", { className: `rounded-md border p-3 text-sm ${result.ok ? 'border-green-500/30 bg-green-500/5 text-green-700 dark:text-green-400' : 'border-destructive/30 bg-destructive/5 text-destructive'}`, children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 shrink-0", "aria-hidden": "true" }), _jsx("div", { children: result.message })] }) })), loading && items.length === 0 ? (_jsx("div", { className: "grid gap-3", children: Array.from({ length: 3 }).map((_, i) => (_jsx(Skeleton, { className: "h-20 w-full" }, i))) })) : items.length === 0 ? (_jsxs("div", { className: "text-center py-12 text-sm text-muted-foreground border rounded-md", children: [_jsx("p", { children: "No marketplace apps installed in this runtime yet." }), _jsx(Button, { variant: "link", className: "mt-2", onClick: () => navigate(`${basePath}/system/marketplace`), children: "Browse the marketplace \u2192" })] })) : (_jsx("div", { className: "grid gap-3", children: items.map((entry) => (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-start justify-between gap-3 pb-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs(CardTitle, { className: "text-base truncate flex items-center gap-2", children: [entry.manifestId, _jsxs(Badge, { variant: "outline", children: ["v", entry.version] })] }), _jsxs("div", { className: "text-xs text-muted-foreground mt-1 flex flex-wrap gap-x-3 gap-y-1", children: [_jsxs("span", { children: ["Installed ", new Date(entry.installedAt).toLocaleString()] }), entry.installedBy && _jsxs("span", { children: ["by ", entry.installedBy] }), _jsxs("span", { children: ["package ", _jsx("code", { className: "font-mono", children: entry.packageId })] })] })] }), _jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`${basePath}/system/marketplace/${entry.packageId}`), children: [_jsx(ExternalLink, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Details"] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void doUninstall(entry), disabled: working === entry.manifestId, children: [_jsx(Trash2, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), working === entry.manifestId ? 'Uninstalling…' : 'Uninstall'] })] })] }), _jsxs(CardContent, { className: "pt-0 text-xs text-muted-foreground", children: ["Cached as ", _jsxs("code", { className: "font-mono", children: [".objectstack/installed-packages/", entry.manifestId.replace(/[^a-zA-Z0-9._-]/g, '_'), ".json"] })] })] }, entry.manifestId))) })), _jsxs("p", { className: "text-xs text-muted-foreground border-t pt-4", children: [_jsx("strong", { children: "Note:" }), " The kernel API is additive only \u2014 uninstall removes the on-disk manifest so the package won't load on next boot, but the running kernel keeps the app registered until you restart the runtime."] })] }));
|
|
64
69
|
}
|
|
@@ -9,14 +9,19 @@ import { useEffect, useState } from 'react';
|
|
|
9
9
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
10
10
|
import { Button, Badge, Card, CardContent, CardHeader, CardTitle, Skeleton, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Label, Checkbox, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, } from '@object-ui/components';
|
|
11
11
|
import { ArrowLeft, ExternalLink, Download, AlertCircle, Package, Trash2, MoreHorizontal, CheckCircle2 } from 'lucide-react';
|
|
12
|
+
import { useIsWorkspaceAdmin } from '@object-ui/auth';
|
|
12
13
|
import { PackageIcon } from './PackageIcon';
|
|
13
14
|
import { MarkdownText } from './MarkdownText';
|
|
15
|
+
import { MarketplaceAccessDenied } from './MarketplaceAccessDenied';
|
|
14
16
|
import { getMarketplacePackage, installPackage, installLocal, uninstallLocal, listLocalInstalls, listCloudEnvironments, listInstallableOrgIds, cloudInstallDeepLink, } from './marketplaceApi';
|
|
15
17
|
import { getRuntimeConfig } from '../../runtime-config';
|
|
18
|
+
import { useMetadata } from '../../providers/MetadataProvider';
|
|
16
19
|
export function MarketplacePackagePage() {
|
|
17
20
|
const navigate = useNavigate();
|
|
18
21
|
const { packageId, appName } = useParams();
|
|
22
|
+
const isAdmin = useIsWorkspaceAdmin();
|
|
19
23
|
const basePath = appName ? `/apps/${appName}` : '';
|
|
24
|
+
const { refresh: refreshMetadata } = useMetadata();
|
|
20
25
|
const [data, setData] = useState(null);
|
|
21
26
|
const [loading, setLoading] = useState(true);
|
|
22
27
|
const [error, setError] = useState(null);
|
|
@@ -70,6 +75,18 @@ export function MarketplacePackagePage() {
|
|
|
70
75
|
setInstallOpen(true);
|
|
71
76
|
setInstallResult(null);
|
|
72
77
|
setEnvsError(null);
|
|
78
|
+
// When the runtime tells us which env we're on (tenant subdomain
|
|
79
|
+
// like demo.objectos.app), skip the picker entirely — the operator's
|
|
80
|
+
// domain already identifies the target. Just open the dialog with
|
|
81
|
+
// the env pre-selected so the sample-data checkbox + Install button
|
|
82
|
+
// remain in place.
|
|
83
|
+
const currentEnvId = getRuntimeConfig().defaultEnvironmentId;
|
|
84
|
+
if (currentEnvId) {
|
|
85
|
+
setEnvsLoading(false);
|
|
86
|
+
setSelectedEnv(currentEnvId);
|
|
87
|
+
setEnvs([]);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
73
90
|
setEnvsLoading(true);
|
|
74
91
|
try {
|
|
75
92
|
const [list, adminOrgIds] = await Promise.all([
|
|
@@ -134,9 +151,15 @@ export function MarketplacePackagePage() {
|
|
|
134
151
|
setLocalResult(null);
|
|
135
152
|
try {
|
|
136
153
|
const result = await installLocal({ packageId });
|
|
154
|
+
try {
|
|
155
|
+
await refreshMetadata('app');
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Non-fatal: the install succeeded; the app list will refresh on next navigation.
|
|
159
|
+
}
|
|
137
160
|
setLocalResult({
|
|
138
161
|
ok: true,
|
|
139
|
-
message: `Installed v${result.version} to this runtime.
|
|
162
|
+
message: `Installed v${result.version} to this runtime. "${data?.package?.display_name ?? result.manifestId}" should now appear in the app switcher.`,
|
|
140
163
|
});
|
|
141
164
|
}
|
|
142
165
|
catch (e) {
|
|
@@ -173,6 +196,12 @@ export function MarketplacePackagePage() {
|
|
|
173
196
|
setLocalResult(null);
|
|
174
197
|
try {
|
|
175
198
|
await uninstallLocal(localInstall.manifestId);
|
|
199
|
+
try {
|
|
200
|
+
await refreshMetadata('app');
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Non-fatal.
|
|
204
|
+
}
|
|
176
205
|
setLocalResult({
|
|
177
206
|
ok: true,
|
|
178
207
|
message: `Removed cached manifest for ${localInstall.manifestId}. Restart the runtime to fully unload the app from the running kernel.`,
|
|
@@ -209,6 +238,10 @@ export function MarketplacePackagePage() {
|
|
|
209
238
|
label: installing ? 'Installing…' : 'Install to cloud…',
|
|
210
239
|
onClick: openInstall,
|
|
211
240
|
};
|
|
241
|
+
if (!isAdmin)
|
|
242
|
+
return _jsx(MarketplaceAccessDenied, {});
|
|
212
243
|
return (_jsxs("div", { className: "mx-auto w-full max-w-6xl flex flex-col gap-6 p-4 sm:p-6", children: [_jsxs(Button, { variant: "ghost", size: "sm", className: "self-start -ml-2 text-muted-foreground hover:text-foreground", onClick: () => navigate(`${basePath}/system/marketplace`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Back to marketplace"] }), _jsxs("div", { className: "flex items-start gap-5 flex-wrap sm:flex-nowrap rounded-2xl border bg-gradient-to-br from-primary/5 via-background to-background p-6 sm:p-8", children: [_jsx(PackageIcon, { iconUrl: pkg.icon_url, displayName: pkg.display_name, manifestId: pkg.manifest_id, className: "h-20 w-20 rounded-2xl shadow-sm ring-1 ring-border shrink-0", initialClassName: "text-3xl font-bold" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("h1", { className: "text-2xl sm:text-3xl font-bold tracking-tight truncate", children: pkg.display_name || pkg.manifest_id }), pkg.homepage_url && (_jsxs("a", { href: pkg.homepage_url, target: "_blank", rel: "noopener noreferrer", className: "inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors", title: "Homepage", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5", "aria-hidden": "true" }), _jsx("span", { className: "hidden sm:inline", children: "Homepage" })] }))] }), _jsxs("div", { className: "text-sm text-muted-foreground mt-2 flex flex-wrap items-center gap-1.5", children: [_jsx("code", { className: "font-mono text-xs px-1.5 py-0.5 rounded bg-muted", children: pkg.manifest_id }), latestVersion && _jsxs(Badge, { variant: "outline", children: ["v", latestVersion] }), pkg.publisher && pkg.publisher !== 'private' && (_jsx(Badge, { variant: pkg.publisher === 'objectstack' ? 'default' : 'secondary', children: pkg.publisher })), pkg.category && _jsx(Badge, { variant: "outline", children: pkg.category }), pkg.license && _jsx(Badge, { variant: "outline", className: "font-normal", children: pkg.license }), localInstall && (_jsxs(Badge, { variant: "default", className: "bg-green-600 hover:bg-green-600 gap-1", children: [_jsx(CheckCircle2, { className: "h-3 w-3", "aria-hidden": "true" }), "Installed \u00B7 v", localInstall.version] }))] }), pkg.description && (_jsx("p", { className: "text-sm sm:text-base text-foreground/80 mt-3 max-w-2xl leading-relaxed", children: pkg.description }))] }), _jsxs("div", { className: "flex items-center gap-2 shrink-0 self-start", children: [_jsxs(Button, { onClick: primaryAction.onClick, disabled: primaryDisabled, size: "lg", className: "min-w-[8rem]", children: [_jsx(Download, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), primaryAction.label] }), localInstall && (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", size: "lg", className: "px-2.5", "aria-label": "More install options", children: _jsx(MoreHorizontal, { className: "h-4 w-4", "aria-hidden": "true" }) }) }), _jsx(DropdownMenuContent, { align: "end", className: "w-56", children: _jsxs(DropdownMenuItem, { onSelect: doUninstallLocal, disabled: installingLocal, className: "text-destructive focus:text-destructive", children: [_jsx(Trash2, { className: "h-4 w-4 mr-2", "aria-hidden": "true" }), "Uninstall from this runtime"] }) })] }))] })] }), localResult && (_jsxs("div", { role: "status", className: `flex items-start gap-2 rounded-md border p-3 text-sm whitespace-pre-wrap ${localResult.ok ? 'border-green-500/30 bg-green-500/5 text-green-700 dark:text-green-400' : 'border-destructive/30 bg-destructive/5 text-destructive'}`, children: [localResult.ok ? _jsx(CheckCircle2, { className: "h-4 w-4 mt-0.5 shrink-0", "aria-hidden": "true" }) : _jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 shrink-0", "aria-hidden": "true" }), _jsx("div", { className: "flex-1", children: localResult.message }), _jsx("button", { type: "button", className: "text-xs underline opacity-60 hover:opacity-100", onClick: () => setLocalResult(null), children: "Dismiss" })] })), _jsxs("div", { className: "grid gap-4 lg:grid-cols-3", children: [_jsx("div", { className: "lg:col-span-2 space-y-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "About" }) }), _jsx(CardContent, { children: pkg.readme ? (_jsx(MarkdownText, { source: pkg.readme })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: "No readme provided." })) })] }) }), _jsx("div", { className: "space-y-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Versions" }) }), _jsx(CardContent, { children: data.versions.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "No approved versions." })) : (_jsx("ul", { className: "space-y-2", children: data.versions.map((v) => (_jsxs("li", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Package, { className: "h-3.5 w-3.5 text-muted-foreground", "aria-hidden": "true" }), _jsxs("code", { className: "font-mono", children: ["v", v.version] }), v.is_prerelease && _jsx(Badge, { variant: "outline", className: "text-xs", children: "pre" })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: v.published_at ? new Date(v.published_at).toLocaleDateString() : '—' })] }, v.id))) })) })] }) })] }), _jsx(Dialog, { open: installOpen, onOpenChange: (o) => { setInstallOpen(o); if (!o)
|
|
213
|
-
setInstallResult(null); }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Install ", pkg.display_name || pkg.manifest_id] }), _jsx(DialogDescription, { children:
|
|
244
|
+
setInstallResult(null); }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Install ", pkg.display_name || pkg.manifest_id] }), _jsx(DialogDescription, { children: getRuntimeConfig().defaultEnvironmentId
|
|
245
|
+
? `Install into this environment (${typeof window !== 'undefined' ? window.location.host : ''}).`
|
|
246
|
+
: 'Choose an environment to install this app into. You need to be signed into ObjectStack Cloud.' })] }), envsLoading ? (_jsx(Skeleton, { className: "h-10 w-full" })) : envsError ? (_jsxs("div", { className: "rounded-md border border-amber-500/30 bg-amber-500/5 p-3 text-sm space-y-2", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 text-amber-600", "aria-hidden": "true" }), _jsx("div", { className: "flex-1", children: envsError })] }), _jsx("a", { href: cloudInstallDeepLink(pkg.id), target: "_blank", rel: "noopener noreferrer", children: _jsxs(Button, { variant: "outline", size: "sm", className: "w-full", children: [_jsx(ExternalLink, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Open on cloud"] }) })] })) : getRuntimeConfig().defaultEnvironmentId ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "seed", checked: seedSampleData, onCheckedChange: (c) => setSeedSampleData(c === true) }), _jsx(Label, { htmlFor: "seed", className: "text-sm font-normal cursor-pointer", children: "Include sample data" })] })) : envs.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "No environments found in your active organization." })) : (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "env-select", children: "Environment" }), _jsxs(Select, { value: selectedEnv, onValueChange: setSelectedEnv, children: [_jsx(SelectTrigger, { id: "env-select", children: _jsx(SelectValue, { placeholder: "Pick an environment" }) }), _jsx(SelectContent, { children: envs.map((e) => (_jsxs(SelectItem, { value: e.id, children: [e.display_name || e.hostname || e.id, e.plan && _jsxs("span", { className: "text-muted-foreground", children: [" \u00B7 ", e.plan] })] }, e.id))) })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "seed", checked: seedSampleData, onCheckedChange: (c) => setSeedSampleData(c === true) }), _jsx(Label, { htmlFor: "seed", className: "text-sm font-normal cursor-pointer", children: "Include sample data" })] })] })), installResult && (_jsx("div", { className: `rounded-md border p-3 text-sm ${installResult.ok ? 'border-green-500/30 bg-green-500/5 text-green-700' : 'border-destructive/30 bg-destructive/5 text-destructive'}`, children: installResult.message })), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setInstallOpen(false), children: "Close" }), !envsError && (_jsx(Button, { onClick: doInstall, disabled: !selectedEnv || installing || installResult?.ok === true, children: installing ? 'Installing…' : 'Install' }))] })] }) })] }));
|
|
214
247
|
}
|
|
@@ -10,7 +10,9 @@ import { useEffect, useMemo, useState } from 'react';
|
|
|
10
10
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
11
11
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription, Badge, Input, Button, Skeleton, } from '@object-ui/components';
|
|
12
12
|
import { Package, Search, RefreshCcw, Store, AlertCircle, CheckCircle2, Settings } from 'lucide-react';
|
|
13
|
+
import { useIsWorkspaceAdmin } from '@object-ui/auth';
|
|
13
14
|
import { PackageIcon } from './PackageIcon';
|
|
15
|
+
import { MarketplaceAccessDenied } from './MarketplaceAccessDenied';
|
|
14
16
|
import { listMarketplacePackages, listLocalInstalls, } from './marketplaceApi';
|
|
15
17
|
function formatRelative(iso) {
|
|
16
18
|
if (!iso)
|
|
@@ -30,6 +32,7 @@ function formatRelative(iso) {
|
|
|
30
32
|
export function MarketplacePage() {
|
|
31
33
|
const navigate = useNavigate();
|
|
32
34
|
const { appName } = useParams();
|
|
35
|
+
const isAdmin = useIsWorkspaceAdmin();
|
|
33
36
|
const basePath = appName ? `/apps/${appName}` : '';
|
|
34
37
|
const [items, setItems] = useState([]);
|
|
35
38
|
const [loading, setLoading] = useState(true);
|
|
@@ -82,6 +85,8 @@ export function MarketplacePage() {
|
|
|
82
85
|
return hay.includes(q);
|
|
83
86
|
});
|
|
84
87
|
}, [items, query, category]);
|
|
88
|
+
if (!isAdmin)
|
|
89
|
+
return _jsx(MarketplaceAccessDenied, {});
|
|
85
90
|
return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Store, { className: "h-6 w-6 text-primary", "aria-hidden": "true" }), _jsx("h1", { className: "text-xl sm:text-2xl font-bold tracking-tight", children: "App Marketplace" })] }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Browse approved apps published to the ObjectStack catalog. Click an app to view details and install it into one of your environments." })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => navigate(`${basePath}/system/marketplace/installed`), children: [_jsx(Settings, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Installed", installed.length > 0 ? ` (${installed.length})` : ''] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void load(), disabled: loading, children: [_jsx(RefreshCcw, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Refresh"] })] })] }), _jsxs("div", { className: "flex flex-col sm:flex-row gap-3", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground", "aria-hidden": "true" }), _jsx(Input, { value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search apps by name or manifest ID\u2026", className: "pl-9", "aria-label": "Search marketplace apps" })] }), categories.length > 0 && (_jsxs("div", { className: "flex flex-wrap gap-1.5", children: [_jsx(Button, { size: "sm", variant: category === '' ? 'default' : 'outline', onClick: () => setCategory(''), children: "All" }), categories.map((cat) => (_jsx(Button, { size: "sm", variant: category === cat ? 'default' : 'outline', onClick: () => setCategory(cat), children: cat }, cat)))] }))] }), error && (_jsxs("div", { className: "flex items-start gap-3 rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 text-destructive", "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("div", { className: "font-medium text-destructive", children: "Failed to load marketplace" }), _jsx("div", { className: "text-muted-foreground mt-1", children: error }), _jsxs("div", { className: "text-xs text-muted-foreground mt-2", children: ["By default this runtime points at the public ObjectStack cloud. Check the runtime is online, or override ", _jsx("code", { className: "font-mono", children: "OS_CLOUD_URL" }), " to point at a self-hosted control plane."] })] })] })), loading && items.length === 0 ? (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-2", children: [_jsx(Skeleton, { className: "h-10 w-10 rounded-lg mb-2" }), _jsx(Skeleton, { className: "h-5 w-3/4" }), _jsx(Skeleton, { className: "h-4 w-1/2 mt-1" })] }), _jsx(CardContent, { children: _jsx(Skeleton, { className: "h-12 w-full" }) })] }, i))) })) : filtered.length === 0 ? (_jsx("div", { className: "text-center py-12 text-sm text-muted-foreground", children: items.length === 0 ? 'No apps have been approved for the marketplace yet.' : 'No apps match your filters.' })) : (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: filtered.map((pkg) => {
|
|
86
91
|
const localEntry = installedByManifestId.get(pkg.manifest_id);
|
|
87
92
|
return (_jsxs(Card, { className: "cursor-pointer transition-colors hover:bg-accent/50 flex flex-col", onClick: () => navigate(`${basePath}/system/marketplace/${pkg.id}`), role: "link", tabIndex: 0, onKeyDown: (e) => {
|
|
@@ -387,6 +387,18 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
387
387
|
if (shouldRefresh)
|
|
388
388
|
setActionRefreshKey(k => k + 1);
|
|
389
389
|
const result = json?.data;
|
|
390
|
+
// ── redirectUrl convention ────────────────────────────────────────
|
|
391
|
+
// A script-action handler can return `{ redirectUrl: 'https://…' }`
|
|
392
|
+
// to ask the UI to open the URL in a new tab. Used by the
|
|
393
|
+
// `sso_as_owner` action on sys_environment to drop the operator
|
|
394
|
+
// into the env runtime as its admin. Same pattern works for any
|
|
395
|
+
// future "deep link" style action.
|
|
396
|
+
if (result && typeof result === 'object' && typeof result.redirectUrl === 'string') {
|
|
397
|
+
try {
|
|
398
|
+
window.open(result.redirectUrl, '_blank', 'noopener,noreferrer');
|
|
399
|
+
}
|
|
400
|
+
catch { /* popup blocked — fall through */ }
|
|
401
|
+
}
|
|
390
402
|
return { success: true, data: result, reload: shouldRefresh };
|
|
391
403
|
}
|
|
392
404
|
catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/app-shell",
|
|
3
|
-
"version": "6.0
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
|
|
@@ -28,46 +28,46 @@
|
|
|
28
28
|
"@sentry/react": "^8.55.2",
|
|
29
29
|
"lucide-react": "^1.16.0",
|
|
30
30
|
"sonner": "^2.0.7",
|
|
31
|
-
"@object-ui/auth": "6.0
|
|
32
|
-
"@object-ui/collaboration": "6.0
|
|
33
|
-
"@object-ui/components": "6.0
|
|
34
|
-
"@object-ui/core": "6.0
|
|
35
|
-
"@object-ui/data-objectstack": "6.0
|
|
36
|
-
"@object-ui/fields": "6.0
|
|
37
|
-
"@object-ui/i18n": "6.0
|
|
38
|
-
"@object-ui/layout": "6.0
|
|
39
|
-
"@object-ui/permissions": "6.0
|
|
40
|
-
"@object-ui/providers": "6.0
|
|
41
|
-
"@object-ui/react": "6.0
|
|
42
|
-
"@object-ui/types": "6.0
|
|
31
|
+
"@object-ui/auth": "6.1.0",
|
|
32
|
+
"@object-ui/collaboration": "6.1.0",
|
|
33
|
+
"@object-ui/components": "6.1.0",
|
|
34
|
+
"@object-ui/core": "6.1.0",
|
|
35
|
+
"@object-ui/data-objectstack": "6.1.0",
|
|
36
|
+
"@object-ui/fields": "6.1.0",
|
|
37
|
+
"@object-ui/i18n": "6.1.0",
|
|
38
|
+
"@object-ui/layout": "6.1.0",
|
|
39
|
+
"@object-ui/permissions": "6.1.0",
|
|
40
|
+
"@object-ui/providers": "6.1.0",
|
|
41
|
+
"@object-ui/react": "6.1.0",
|
|
42
|
+
"@object-ui/types": "6.1.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"react": "^18.0.0 || ^19.0.0",
|
|
46
46
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
47
47
|
"react-router-dom": "^6.0.0 || ^7.0.0",
|
|
48
|
-
"@object-ui/plugin-calendar": "^6.0
|
|
49
|
-
"@object-ui/plugin-charts": "^6.0
|
|
50
|
-
"@object-ui/plugin-chatbot": "^6.0
|
|
51
|
-
"@object-ui/plugin-dashboard": "^6.0
|
|
52
|
-
"@object-ui/plugin-designer": "^6.0
|
|
53
|
-
"@object-ui/plugin-detail": "^6.0
|
|
54
|
-
"@object-ui/plugin-form": "^6.0
|
|
55
|
-
"@object-ui/plugin-grid": "^6.0
|
|
56
|
-
"@object-ui/plugin-kanban": "^6.0
|
|
57
|
-
"@object-ui/plugin-list": "^6.0
|
|
58
|
-
"@object-ui/plugin-report": "^6.0
|
|
59
|
-
"@object-ui/plugin-view": "^6.0
|
|
48
|
+
"@object-ui/plugin-calendar": "^6.1.0",
|
|
49
|
+
"@object-ui/plugin-charts": "^6.1.0",
|
|
50
|
+
"@object-ui/plugin-chatbot": "^6.1.0",
|
|
51
|
+
"@object-ui/plugin-dashboard": "^6.1.0",
|
|
52
|
+
"@object-ui/plugin-designer": "^6.1.0",
|
|
53
|
+
"@object-ui/plugin-detail": "^6.1.0",
|
|
54
|
+
"@object-ui/plugin-form": "^6.1.0",
|
|
55
|
+
"@object-ui/plugin-grid": "^6.1.0",
|
|
56
|
+
"@object-ui/plugin-kanban": "^6.1.0",
|
|
57
|
+
"@object-ui/plugin-list": "^6.1.0",
|
|
58
|
+
"@object-ui/plugin-report": "^6.1.0",
|
|
59
|
+
"@object-ui/plugin-view": "^6.1.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@types/node": "^25.9.
|
|
63
|
-
"@types/react": "19.2.
|
|
62
|
+
"@types/node": "^25.9.1",
|
|
63
|
+
"@types/react": "19.2.15",
|
|
64
64
|
"@types/react-dom": "19.2.3",
|
|
65
65
|
"react": "19.2.6",
|
|
66
66
|
"react-dom": "19.2.6",
|
|
67
67
|
"react-router-dom": "^7.15.0",
|
|
68
68
|
"sonner": "^2.0.7",
|
|
69
69
|
"typescript": "^6.0.3",
|
|
70
|
-
"vite": "^8.0.
|
|
70
|
+
"vite": "^8.0.14"
|
|
71
71
|
},
|
|
72
72
|
"keywords": [
|
|
73
73
|
"objectui",
|