@object-ui/app-shell 5.0.2 → 5.2.1

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +401 -0
  2. package/dist/chrome/CommandPalette.d.ts +6 -1
  3. package/dist/chrome/CommandPalette.js +40 -4
  4. package/dist/chrome/ConsoleToaster.js +8 -1
  5. package/dist/chrome/RouteFader.d.ts +32 -0
  6. package/dist/chrome/RouteFader.js +52 -0
  7. package/dist/chrome/index.d.ts +2 -0
  8. package/dist/chrome/index.js +2 -0
  9. package/dist/chrome/toast-helpers.d.ts +48 -0
  10. package/dist/chrome/toast-helpers.js +59 -0
  11. package/dist/console/AppContent.js +108 -2
  12. package/dist/console/home/HomePage.js +5 -4
  13. package/dist/console/marketplace/MarketplaceInstalledPage.d.ts +17 -0
  14. package/dist/console/marketplace/MarketplaceInstalledPage.js +64 -0
  15. package/dist/console/marketplace/MarketplacePackagePage.d.ts +7 -0
  16. package/dist/console/marketplace/MarketplacePackagePage.js +199 -0
  17. package/dist/console/marketplace/MarketplacePage.d.ts +8 -0
  18. package/dist/console/marketplace/MarketplacePage.js +94 -0
  19. package/dist/console/marketplace/PackageIcon.d.ts +19 -0
  20. package/dist/console/marketplace/PackageIcon.js +17 -0
  21. package/dist/console/marketplace/marketplaceApi.d.ts +122 -0
  22. package/dist/console/marketplace/marketplaceApi.js +207 -0
  23. package/dist/index.d.ts +5 -6
  24. package/dist/index.js +4 -5
  25. package/dist/layout/AppHeader.js +25 -21
  26. package/dist/layout/AppSidebar.js +15 -11
  27. package/dist/layout/InboxPopover.js +43 -3
  28. package/dist/types.d.ts +0 -46
  29. package/dist/views/ObjectView.js +20 -7
  30. package/dist/views/RecordDetailView.js +213 -45
  31. package/package.json +25 -25
  32. package/src/styles.css +49 -0
  33. package/dist/components/DashboardRenderer.d.ts +0 -8
  34. package/dist/components/DashboardRenderer.js +0 -16
  35. package/dist/components/FormRenderer.d.ts +0 -8
  36. package/dist/components/FormRenderer.js +0 -31
  37. package/dist/components/ObjectRenderer.d.ts +0 -8
  38. package/dist/components/ObjectRenderer.js +0 -74
  39. package/dist/components/PageRenderer.d.ts +0 -7
  40. package/dist/components/PageRenderer.js +0 -14
@@ -0,0 +1,94 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Marketplace Page — browse approved, marketplace-listed packages.
4
+ *
5
+ * Card-grid catalog of public packages, fetched from the tenant runtime's
6
+ * `/api/v1/marketplace/packages` proxy (which forwards to the configured
7
+ * cloud control plane). Click-through opens the package detail page.
8
+ */
9
+ import { useEffect, useMemo, useState } from 'react';
10
+ import { useNavigate, useParams } from 'react-router-dom';
11
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription, Badge, Input, Button, Skeleton, } from '@object-ui/components';
12
+ import { Package, Search, RefreshCcw, Store, AlertCircle, CheckCircle2, Settings } from 'lucide-react';
13
+ import { PackageIcon } from './PackageIcon';
14
+ import { listMarketplacePackages, listLocalInstalls, } from './marketplaceApi';
15
+ function formatRelative(iso) {
16
+ if (!iso)
17
+ return '';
18
+ const d = new Date(iso);
19
+ if (Number.isNaN(d.getTime()))
20
+ return '';
21
+ const days = Math.floor((Date.now() - d.getTime()) / 86400000);
22
+ if (days < 1)
23
+ return 'today';
24
+ if (days < 30)
25
+ return `${days}d ago`;
26
+ if (days < 365)
27
+ return `${Math.floor(days / 30)}mo ago`;
28
+ return `${Math.floor(days / 365)}y ago`;
29
+ }
30
+ export function MarketplacePage() {
31
+ const navigate = useNavigate();
32
+ const { appName } = useParams();
33
+ const basePath = appName ? `/apps/${appName}` : '';
34
+ const [items, setItems] = useState([]);
35
+ const [loading, setLoading] = useState(true);
36
+ const [error, setError] = useState(null);
37
+ const [query, setQuery] = useState('');
38
+ const [category, setCategory] = useState('');
39
+ const [installed, setInstalled] = useState([]);
40
+ const installedByManifestId = useMemo(() => {
41
+ const m = new Map();
42
+ for (const e of installed)
43
+ m.set(e.manifestId, e);
44
+ return m;
45
+ }, [installed]);
46
+ const load = async () => {
47
+ setLoading(true);
48
+ setError(null);
49
+ try {
50
+ const [resp, installs] = await Promise.all([
51
+ listMarketplacePackages({ limit: 100 }),
52
+ listLocalInstalls(),
53
+ ]);
54
+ setItems(resp.items ?? []);
55
+ setInstalled(installs);
56
+ }
57
+ catch (e) {
58
+ setError(e?.message ?? String(e));
59
+ setItems([]);
60
+ }
61
+ finally {
62
+ setLoading(false);
63
+ }
64
+ };
65
+ useEffect(() => { void load(); }, []);
66
+ const categories = useMemo(() => {
67
+ const s = new Set();
68
+ for (const it of items) {
69
+ if (it.category)
70
+ s.add(it.category);
71
+ }
72
+ return Array.from(s).sort();
73
+ }, [items]);
74
+ const filtered = useMemo(() => {
75
+ const q = query.trim().toLowerCase();
76
+ return items.filter((it) => {
77
+ if (category && it.category !== category)
78
+ return false;
79
+ if (!q)
80
+ return true;
81
+ const hay = `${it.display_name} ${it.manifest_id} ${it.description ?? ''}`.toLowerCase();
82
+ return hay.includes(q);
83
+ });
84
+ }, [items, query, category]);
85
+ 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: ["Check that this runtime is configured with ", _jsx("code", { className: "font-mono", children: "OS_CLOUD_URL" }), " pointing at a reachable ObjectStack Cloud."] })] })] })), 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
+ const localEntry = installedByManifestId.get(pkg.manifest_id);
87
+ 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) => {
88
+ if (e.key === 'Enter' || e.key === ' ') {
89
+ e.preventDefault();
90
+ navigate(`${basePath}/system/marketplace/${pkg.id}`);
91
+ }
92
+ }, "data-testid": `marketplace-card-${pkg.manifest_id}`, children: [_jsxs(CardHeader, { className: "flex flex-row items-start gap-3 pb-2", children: [_jsx(PackageIcon, { iconUrl: pkg.icon_url, displayName: pkg.display_name, manifestId: pkg.manifest_id }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(CardTitle, { className: "text-base truncate", children: pkg.display_name || pkg.manifest_id }), _jsx(CardDescription, { className: "text-xs truncate", children: _jsx("code", { className: "font-mono", children: pkg.manifest_id }) })] }), pkg.publisher && pkg.publisher !== 'private' && (_jsx(Badge, { variant: pkg.publisher === 'objectstack' ? 'default' : 'secondary', className: "shrink-0", children: pkg.publisher }))] }), _jsxs(CardContent, { className: "flex-1 flex flex-col gap-2", children: [_jsx("p", { className: "text-sm text-muted-foreground line-clamp-2", children: pkg.description || 'No description provided.' }), _jsxs("div", { className: "flex items-center gap-2 mt-auto pt-2 flex-wrap", children: [localEntry && (_jsxs(Badge, { variant: "default", className: "text-xs bg-green-600 hover:bg-green-600", children: [_jsx(CheckCircle2, { className: "h-3 w-3 mr-1", "aria-hidden": "true" }), "Installed v", localEntry.version] })), pkg.latest_version?.version && (_jsxs(Badge, { variant: "outline", className: "text-xs", children: [_jsx(Package, { className: "h-3 w-3 mr-1", "aria-hidden": "true" }), "v", pkg.latest_version.version] })), pkg.category && (_jsx(Badge, { variant: "outline", className: "text-xs", children: pkg.category })), pkg.latest_version?.published_at && (_jsx("span", { className: "text-xs text-muted-foreground ml-auto", children: formatRelative(pkg.latest_version.published_at) }))] })] })] }, pkg.id));
93
+ }) }))] }));
94
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * PackageIcon — square icon tile for marketplace packages.
3
+ *
4
+ * Renders the package's `icon_url` when present, falling back to the
5
+ * first letter of the display name (or manifest id) on missing-URL,
6
+ * load-error, or empty URL. The fallback also covers the common
7
+ * case where cloud rows point at icon assets that don't yet exist
8
+ * on the configured CDN.
9
+ */
10
+ export interface PackageIconProps {
11
+ iconUrl?: string | null;
12
+ displayName?: string | null;
13
+ manifestId?: string | null;
14
+ /** Tailwind size classes — caller controls outer dimensions. */
15
+ className?: string;
16
+ /** Text size for the initial-letter fallback. */
17
+ initialClassName?: string;
18
+ }
19
+ export declare function PackageIcon({ iconUrl, displayName, manifestId, className, initialClassName, }: PackageIconProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * PackageIcon — square icon tile for marketplace packages.
4
+ *
5
+ * Renders the package's `icon_url` when present, falling back to the
6
+ * first letter of the display name (or manifest id) on missing-URL,
7
+ * load-error, or empty URL. The fallback also covers the common
8
+ * case where cloud rows point at icon assets that don't yet exist
9
+ * on the configured CDN.
10
+ */
11
+ import { useState } from 'react';
12
+ export function PackageIcon({ iconUrl, displayName, manifestId, className = 'h-10 w-10', initialClassName = 'text-base font-semibold', }) {
13
+ const [broken, setBroken] = useState(false);
14
+ const initial = ((displayName ?? manifestId ?? '?').trim()[0] ?? '?').toUpperCase();
15
+ const showImg = !!iconUrl && !broken;
16
+ return (_jsx("div", { className: `flex shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary overflow-hidden ${className}`, children: showImg ? (_jsx("img", { src: iconUrl, alt: "", className: `${className} object-cover`, onError: () => setBroken(true), loading: "lazy" })) : (_jsx("span", { className: initialClassName, children: initial })) }));
17
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Marketplace REST helper.
3
+ *
4
+ * Thin fetch wrapper around the tenant runtime's marketplace proxy
5
+ * (`/api/v1/marketplace/*`). The proxy forwards to the configured
6
+ * cloud control plane (`OS_CLOUD_URL`) — only browse endpoints are
7
+ * exposed. Install is performed against the cloud action endpoint
8
+ * directly (via `installPackage()` below), which requires the user
9
+ * to have a cloud session.
10
+ *
11
+ * See:
12
+ * cloud/packages/service-cloud/src/routes/marketplace.ts
13
+ * framework/packages/runtime/src/cloud/marketplace-proxy-plugin.ts
14
+ */
15
+ export interface MarketplacePackageSummary {
16
+ id: string;
17
+ manifest_id: string;
18
+ display_name: string;
19
+ description?: string | null;
20
+ category?: string | null;
21
+ tags?: string | null;
22
+ icon_url?: string | null;
23
+ homepage_url?: string | null;
24
+ license?: string | null;
25
+ publisher?: string | null;
26
+ is_starter?: boolean;
27
+ created_at?: string;
28
+ updated_at?: string;
29
+ latest_version: MarketplacePackageVersion | null;
30
+ }
31
+ export interface MarketplacePackageDetail extends MarketplacePackageSummary {
32
+ readme?: string | null;
33
+ }
34
+ export interface MarketplacePackageVersion {
35
+ id: string;
36
+ version: string;
37
+ status?: string;
38
+ is_prerelease?: boolean;
39
+ published_at?: string | null;
40
+ listing_status?: string;
41
+ reviewed_at?: string | null;
42
+ }
43
+ export interface MarketplaceListResponse {
44
+ items: MarketplacePackageSummary[];
45
+ total: number;
46
+ limit: number;
47
+ offset: number;
48
+ }
49
+ export interface MarketplaceDetailResponse {
50
+ package: MarketplacePackageDetail;
51
+ versions: MarketplacePackageVersion[];
52
+ }
53
+ export declare function listMarketplacePackages(params?: {
54
+ q?: string;
55
+ category?: string;
56
+ limit?: number;
57
+ offset?: number;
58
+ }): Promise<MarketplaceListResponse>;
59
+ export declare function getMarketplacePackage(id: string): Promise<MarketplaceDetailResponse>;
60
+ export interface InstallResponse {
61
+ installation?: {
62
+ id: string;
63
+ environment_id: string;
64
+ package_id: string;
65
+ version: string;
66
+ };
67
+ message?: string;
68
+ [k: string]: any;
69
+ }
70
+ export declare function installPackage(input: {
71
+ packageId: string;
72
+ environmentId: string;
73
+ seedSampleData?: boolean;
74
+ }): Promise<InstallResponse>;
75
+ /**
76
+ * Helper: list environments visible to the current cloud user (active
77
+ * organisation). Used to populate the install dialog's environment picker.
78
+ * Calls cloud directly — same auth model as `installPackage`.
79
+ */
80
+ export interface CloudEnvironment {
81
+ id: string;
82
+ display_name?: string;
83
+ hostname?: string;
84
+ organization_id?: string;
85
+ plan?: string;
86
+ status?: string;
87
+ }
88
+ export declare function listCloudEnvironments(): Promise<CloudEnvironment[]>;
89
+ /**
90
+ * List orgs in which the current cloud user has an `owner` or `admin`
91
+ * role on `sys_member`. Used to filter the install dialog's env picker
92
+ * — only envs whose `organization_id` is in this set are installable
93
+ * (the backend enforces the same gate; this is the UX mirror).
94
+ *
95
+ * Returns an empty set on 401 / network failure so the install dialog
96
+ * can render a clean "no installable environments" state.
97
+ */
98
+ export declare function listInstallableOrgIds(): Promise<Set<string>>;
99
+ export declare function cloudInstallDeepLink(packageId: string): string;
100
+ export interface LocalInstallEntry {
101
+ packageId: string;
102
+ versionId: string;
103
+ manifestId: string;
104
+ version: string;
105
+ installedAt: string;
106
+ installedBy: string | null;
107
+ }
108
+ export interface LocalInstallResult {
109
+ manifestId: string;
110
+ version: string;
111
+ versionId: string;
112
+ installedAt: string;
113
+ hotLoaded: boolean;
114
+ upgradedFrom: string | null;
115
+ note?: string;
116
+ }
117
+ export declare function installLocal(input: {
118
+ packageId: string;
119
+ versionId?: string;
120
+ }): Promise<LocalInstallResult>;
121
+ export declare function listLocalInstalls(): Promise<LocalInstallEntry[]>;
122
+ export declare function uninstallLocal(manifestId: string): Promise<void>;
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Marketplace REST helper.
3
+ *
4
+ * Thin fetch wrapper around the tenant runtime's marketplace proxy
5
+ * (`/api/v1/marketplace/*`). The proxy forwards to the configured
6
+ * cloud control plane (`OS_CLOUD_URL`) — only browse endpoints are
7
+ * exposed. Install is performed against the cloud action endpoint
8
+ * directly (via `installPackage()` below), which requires the user
9
+ * to have a cloud session.
10
+ *
11
+ * See:
12
+ * cloud/packages/service-cloud/src/routes/marketplace.ts
13
+ * framework/packages/runtime/src/cloud/marketplace-proxy-plugin.ts
14
+ */
15
+ const SERVER_URL = (import.meta.env.VITE_SERVER_URL || '').replace(/\/$/, '');
16
+ const API_BASE = `${SERVER_URL}/api/v1/marketplace`;
17
+ async function call(path, init) {
18
+ const res = await fetch(`${API_BASE}${path}`, {
19
+ credentials: 'omit',
20
+ headers: { 'Accept': 'application/json', ...(init?.headers || {}) },
21
+ ...init,
22
+ });
23
+ let payload = null;
24
+ try {
25
+ payload = await res.json();
26
+ }
27
+ catch { /* empty body */ }
28
+ if (!res.ok) {
29
+ const code = payload?.error?.code ?? payload?.code ?? `HTTP_${res.status}`;
30
+ const message = payload?.error?.message ?? payload?.error ?? res.statusText;
31
+ const err = new Error(typeof message === 'string' ? message : `${code}`);
32
+ err.code = code;
33
+ err.status = res.status;
34
+ throw err;
35
+ }
36
+ // Cloud helpers return `{ success: true, data: ... }`; unwrap when present.
37
+ return (payload?.data ?? payload);
38
+ }
39
+ export async function listMarketplacePackages(params = {}) {
40
+ const usp = new URLSearchParams();
41
+ if (params.q)
42
+ usp.set('q', params.q);
43
+ if (params.category)
44
+ usp.set('category', params.category);
45
+ if (params.limit != null)
46
+ usp.set('limit', String(params.limit));
47
+ if (params.offset != null)
48
+ usp.set('offset', String(params.offset));
49
+ const qs = usp.toString();
50
+ return call(`/packages${qs ? `?${qs}` : ''}`);
51
+ }
52
+ export async function getMarketplacePackage(id) {
53
+ return call(`/packages/${encodeURIComponent(id)}`);
54
+ }
55
+ /**
56
+ * Install a package into an environment by calling the cloud's
57
+ * `install_package` action **directly** (not via the proxy). Requires the
58
+ * caller's browser to have a valid cloud session cookie — typically
59
+ * because the user has signed into cloud at least once and the cookie
60
+ * domain covers this origin. When that's not the case the call fails
61
+ * with 401; the UI can then surface a "Sign in on cloud" link.
62
+ *
63
+ * `cloudBaseUrl` is read from `VITE_CLOUD_URL` if set; otherwise we fall
64
+ * back to relative (assumes same-origin, which is true for cloud-hosted
65
+ * consoles but generally not for tenant runtimes).
66
+ */
67
+ const CLOUD_BASE = (import.meta.env.VITE_CLOUD_URL || '').replace(/\/$/, '');
68
+ export async function installPackage(input) {
69
+ const base = CLOUD_BASE || SERVER_URL;
70
+ const res = await fetch(`${base}/api/v1/actions/sys_package/install_package`, {
71
+ method: 'POST',
72
+ credentials: 'include',
73
+ headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
74
+ body: JSON.stringify({
75
+ recordId: input.packageId,
76
+ params: {
77
+ environment_id: input.environmentId,
78
+ seed_sample_data: !!input.seedSampleData,
79
+ },
80
+ }),
81
+ });
82
+ let payload = null;
83
+ try {
84
+ payload = await res.json();
85
+ }
86
+ catch { /* empty */ }
87
+ if (!res.ok) {
88
+ const code = payload?.code ?? payload?.error?.code ?? `HTTP_${res.status}`;
89
+ const message = payload?.error ?? payload?.message ?? res.statusText;
90
+ const err = new Error(typeof message === 'string' ? message : `${code}`);
91
+ err.code = code;
92
+ err.status = res.status;
93
+ throw err;
94
+ }
95
+ return (payload?.data ?? payload);
96
+ }
97
+ export async function listCloudEnvironments() {
98
+ const base = CLOUD_BASE || SERVER_URL;
99
+ const res = await fetch(`${base}/api/v1/data/sys_environment?limit=200`, {
100
+ credentials: 'include',
101
+ headers: { 'Accept': 'application/json' },
102
+ });
103
+ if (!res.ok) {
104
+ const err = new Error(`HTTP_${res.status}`);
105
+ err.status = res.status;
106
+ throw err;
107
+ }
108
+ const payload = await res.json().catch(() => ({}));
109
+ const rows = payload?.data ?? payload?.items ?? payload ?? [];
110
+ return Array.isArray(rows) ? rows : [];
111
+ }
112
+ /**
113
+ * List orgs in which the current cloud user has an `owner` or `admin`
114
+ * role on `sys_member`. Used to filter the install dialog's env picker
115
+ * — only envs whose `organization_id` is in this set are installable
116
+ * (the backend enforces the same gate; this is the UX mirror).
117
+ *
118
+ * Returns an empty set on 401 / network failure so the install dialog
119
+ * can render a clean "no installable environments" state.
120
+ */
121
+ export async function listInstallableOrgIds() {
122
+ const base = CLOUD_BASE || SERVER_URL;
123
+ // sys_member rows are scoped to the caller; better-auth-managed table.
124
+ const url = `${base}/api/v1/data/sys_member?limit=200`;
125
+ let payload = null;
126
+ try {
127
+ const res = await fetch(url, {
128
+ credentials: 'include',
129
+ headers: { 'Accept': 'application/json' },
130
+ });
131
+ if (!res.ok)
132
+ return new Set();
133
+ payload = await res.json().catch(() => ({}));
134
+ }
135
+ catch {
136
+ return new Set();
137
+ }
138
+ const rows = payload?.data ?? payload?.items ?? payload ?? [];
139
+ if (!Array.isArray(rows))
140
+ return new Set();
141
+ const ids = new Set();
142
+ for (const row of rows) {
143
+ const role = String(row?.role ?? '').toLowerCase();
144
+ const orgId = row?.organization_id ?? row?.organizationId;
145
+ if (orgId && (role === 'owner' || role === 'admin')) {
146
+ ids.add(String(orgId));
147
+ }
148
+ }
149
+ return ids;
150
+ }
151
+ export function cloudInstallDeepLink(packageId) {
152
+ const base = CLOUD_BASE || 'https://cloud.objectos.app';
153
+ return `${base}/apps/cloud-control/sys_package/${encodeURIComponent(packageId)}`;
154
+ }
155
+ export async function installLocal(input) {
156
+ const res = await fetch(`${API_BASE}/install-local`, {
157
+ method: 'POST',
158
+ credentials: 'include',
159
+ headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
160
+ body: JSON.stringify({
161
+ packageId: input.packageId,
162
+ ...(input.versionId ? { versionId: input.versionId } : {}),
163
+ }),
164
+ });
165
+ let payload = null;
166
+ try {
167
+ payload = await res.json();
168
+ }
169
+ catch { /* empty */ }
170
+ if (!res.ok) {
171
+ const code = payload?.error?.code ?? `HTTP_${res.status}`;
172
+ const message = payload?.error?.message ?? res.statusText;
173
+ const err = new Error(typeof message === 'string' ? message : `${code}`);
174
+ err.code = code;
175
+ err.status = res.status;
176
+ throw err;
177
+ }
178
+ return (payload?.data ?? payload);
179
+ }
180
+ export async function listLocalInstalls() {
181
+ try {
182
+ const res = await fetch(`${API_BASE}/install-local`, {
183
+ credentials: 'include',
184
+ headers: { 'Accept': 'application/json' },
185
+ });
186
+ if (!res.ok)
187
+ return [];
188
+ const payload = await res.json().catch(() => ({}));
189
+ const items = payload?.data?.items ?? payload?.items ?? [];
190
+ return Array.isArray(items) ? items : [];
191
+ }
192
+ catch {
193
+ return [];
194
+ }
195
+ }
196
+ export async function uninstallLocal(manifestId) {
197
+ const res = await fetch(`${API_BASE}/install-local/${encodeURIComponent(manifestId)}`, {
198
+ method: 'DELETE',
199
+ credentials: 'include',
200
+ headers: { 'Accept': 'application/json' },
201
+ });
202
+ if (!res.ok) {
203
+ const payload = await res.json().catch(() => ({}));
204
+ const message = payload?.error?.message ?? res.statusText;
205
+ throw new Error(typeof message === 'string' ? message : `HTTP_${res.status}`);
206
+ }
207
+ }
package/dist/index.d.ts CHANGED
@@ -5,23 +5,19 @@
5
5
  * Framework-agnostic rendering engine for third-party integration
6
6
  */
7
7
  export { AppShell } from './components/AppShell';
8
- export { ObjectRenderer } from './components/ObjectRenderer';
9
- export { DashboardRenderer } from './components/DashboardRenderer';
10
- export { PageRenderer } from './components/PageRenderer';
11
- export { FormRenderer } from './components/FormRenderer';
12
8
  export { AdapterProvider, useAdapter } from './providers/AdapterProvider';
13
9
  export { MetadataProvider, useMetadata, useMetadataItem } from './providers/MetadataProvider';
14
10
  export { ExpressionProvider, useExpressionContext, evaluateVisibility } from './providers/ExpressionProvider';
15
11
  export { useObjectActions } from './hooks/useObjectActions';
16
12
  export { useRecentItems } from './hooks/useRecentItems';
17
- export type { AppShellProps, ObjectRendererProps, DashboardRendererProps, PageRendererProps, FormRendererProps, } from './types';
13
+ export type { AppShellProps, } from './types';
18
14
  export type { MetadataState, MetadataContextValue, MetadataTypeStatus, } from './providers/MetadataProvider';
19
15
  export type { ExpressionContextValue, } from './providers/ExpressionProvider';
20
16
  export type { RecentItem, } from './hooks/useRecentItems';
21
17
  export { ConsoleShell, ConnectedShell, RequireOrganization, AuthenticatedRoute, RootRedirect, SystemRedirect, LoadingFallback, } from './console/ConsoleShell';
22
18
  export { ConsoleLayout, AppHeader, AppSidebar, UnifiedSidebar, AppSwitcher, ConnectionStatus, ActivityFeed, LocaleSwitcher, ModeToggle, AuthPageLayout, } from './layout';
23
19
  export type { ActivityItem } from './layout';
24
- export { CommandPalette, KeyboardShortcutsDialog, OnboardingWalkthrough, ConditionalAuthWrapper, ConsoleToaster, ErrorBoundary, LoadingScreen, ThemeProvider, useTheme, } from './chrome';
20
+ export { CommandPalette, KeyboardShortcutsDialog, OnboardingWalkthrough, ConditionalAuthWrapper, ConsoleToaster, RouteFader, toastWithUndo, type ToastWithUndoOptions, ErrorBoundary, LoadingScreen, ThemeProvider, useTheme, } from './chrome';
25
21
  export { ObjectView, RecordDetailView, RecordFormPage, DashboardView, PageView, ReportView, SearchResultsPage, ViewConfigPanel, } from './views';
26
22
  export type { RecordFormPageProps } from './views';
27
23
  export { useFavorites, useMetadataService, useNavPins, useNavigationSync, NavigationSyncEffect, addNavigationItem, removeNavigationItems, renameNavigationItems, navigationEqual, generateNavId, useResponsiveSidebar, } from './hooks';
@@ -29,6 +25,9 @@ export type { FavoriteItem } from './hooks';
29
25
  export { NavigationProvider, useNavigationContext, FavoritesProvider, RecentItemsProvider, UserStateAdaptersProvider, useUserStateAdapter, useAttachUserStateAdapters, } from './context';
30
26
  export type { UserDataAdapter, UserStateKind } from './context';
31
27
  export { AppContent as DefaultAppContent } from './console/AppContent';
28
+ export { MarketplacePage } from './console/marketplace/MarketplacePage';
29
+ export { MarketplacePackagePage } from './console/marketplace/MarketplacePackagePage';
30
+ export { MarketplaceInstalledPage } from './console/marketplace/MarketplaceInstalledPage';
32
31
  export { LoginPage as DefaultLoginPage } from './console/auth/LoginPage';
33
32
  export { RegisterPage as DefaultRegisterPage } from './console/auth/RegisterPage';
34
33
  export { ForgotPasswordPage as DefaultForgotPasswordPage } from './console/auth/ForgotPasswordPage';
package/dist/index.js CHANGED
@@ -6,10 +6,6 @@
6
6
  */
7
7
  // Components
8
8
  export { AppShell } from './components/AppShell';
9
- export { ObjectRenderer } from './components/ObjectRenderer';
10
- export { DashboardRenderer } from './components/DashboardRenderer';
11
- export { PageRenderer } from './components/PageRenderer';
12
- export { FormRenderer } from './components/FormRenderer';
13
9
  // Providers
14
10
  export { AdapterProvider, useAdapter } from './providers/AdapterProvider';
15
11
  export { MetadataProvider, useMetadata, useMetadataItem } from './providers/MetadataProvider';
@@ -23,7 +19,7 @@ export { ConsoleShell, ConnectedShell, RequireOrganization, AuthenticatedRoute,
23
19
  // Layout chrome
24
20
  export { ConsoleLayout, AppHeader, AppSidebar, UnifiedSidebar, AppSwitcher, ConnectionStatus, ActivityFeed, LocaleSwitcher, ModeToggle, AuthPageLayout, } from './layout';
25
21
  // Top-level chrome (dialogs, providers, error boundaries)
26
- export { CommandPalette, KeyboardShortcutsDialog, OnboardingWalkthrough, ConditionalAuthWrapper, ConsoleToaster, ErrorBoundary, LoadingScreen, ThemeProvider, useTheme, } from './chrome';
22
+ export { CommandPalette, KeyboardShortcutsDialog, OnboardingWalkthrough, ConditionalAuthWrapper, ConsoleToaster, RouteFader, toastWithUndo, ErrorBoundary, LoadingScreen, ThemeProvider, useTheme, } from './chrome';
27
23
  // Standard inner-SPA views
28
24
  export { ObjectView, RecordDetailView, RecordFormPage, DashboardView, PageView, ReportView, SearchResultsPage, ViewConfigPanel, } from './views';
29
25
  // Hooks
@@ -32,6 +28,9 @@ export { useFavorites, useMetadataService, useNavPins, useNavigationSync, Naviga
32
28
  export { NavigationProvider, useNavigationContext, FavoritesProvider, RecentItemsProvider, UserStateAdaptersProvider, useUserStateAdapter, useAttachUserStateAdapters, } from './context';
33
29
  // Default page implementations (consumers can partial-override slots)
34
30
  export { AppContent as DefaultAppContent } from './console/AppContent';
31
+ export { MarketplacePage } from './console/marketplace/MarketplacePage';
32
+ export { MarketplacePackagePage } from './console/marketplace/MarketplacePackagePage';
33
+ export { MarketplaceInstalledPage } from './console/marketplace/MarketplaceInstalledPage';
35
34
  export { LoginPage as DefaultLoginPage } from './console/auth/LoginPage';
36
35
  export { RegisterPage as DefaultRegisterPage } from './console/auth/RegisterPage';
37
36
  export { ForgotPasswordPage as DefaultForgotPasswordPage } from './console/auth/ForgotPasswordPage';
@@ -22,7 +22,7 @@ import { SidebarTrigger, Button, DropdownMenu, DropdownMenuTrigger, DropdownMenu
22
22
  import { Search, HelpCircle, ChevronDown, Check, Lock, Settings, LogOut, User as UserIcon, Boxes, } from 'lucide-react';
23
23
  import { useState, useEffect, useCallback, useRef } from 'react';
24
24
  import { useOffline } from '@object-ui/react';
25
- import { PresenceAvatars } from '@object-ui/collaboration';
25
+ import { PresenceAvatars, useTenantPresence } from '@object-ui/collaboration';
26
26
  import { ModeToggle } from './ModeToggle';
27
27
  import { LocaleSwitcher } from './LocaleSwitcher';
28
28
  import { ConnectionStatus } from './ConnectionStatus';
@@ -58,14 +58,12 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
58
58
  const { apps: metadataApps, dashboards: metadataDashboards, pages: metadataPages, reports: metadataReports } = useMetadata();
59
59
  const { currentAppName, recordTitle } = useNavigationContext();
60
60
  const mobileSwitcher = useMobileViewSwitcher();
61
- const [apiPresenceUsers, setApiPresenceUsers] = useState(null);
62
61
  const [apiActivities, setApiActivities] = useState(null);
63
62
  /** M10.8: in-header notifications. Polled from sys_notification scoped to current user. */
64
63
  const [notifications, setNotifications] = useState([]);
65
64
  // Once the server returns 404 for these collections we stop retrying for
66
65
  // the lifetime of the page — they're optional features and re-requesting
67
66
  // on every navigation creates console noise + wasted round trips.
68
- const presenceUnavailableRef = useRef(false);
69
67
  const activityUnavailableRef = useRef(false);
70
68
  const notificationsUnavailableRef = useRef(false);
71
69
  /** M11.C15: pending approvals count for the topbar shortcut. */
@@ -79,29 +77,21 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
79
77
  // registered on the server. Either signal means the feature is
80
78
  // unavailable — disable it for the rest of the page.
81
79
  const isMissingResource = (err) => err?.httpStatus === 404 || err?.status === 404 || err?.code === 'object_not_found';
82
- const presenceP = presenceUnavailableRef.current
83
- ? Promise.resolve({ data: [] })
84
- : dataSource.find('sys_presence').catch((err) => {
85
- if (isMissingResource(err))
86
- presenceUnavailableRef.current = true;
87
- return { data: [] };
88
- });
89
- const activityP = activityUnavailableRef.current
90
- ? Promise.resolve({ data: [] })
91
- : dataSource
80
+ // Tenant-wide presence ("who else is online?") is intentionally NOT
81
+ // probed here. Presence is real-time ephemeral state that does not
82
+ // belong in a regular REST collection. The feature is staged behind a
83
+ // transport-level provider (<PresenceProvider>) which is not yet
84
+ // wired — see ROADMAP for the realtime plan.
85
+ if (activityUnavailableRef.current)
86
+ return;
87
+ try {
88
+ const activityResult = await dataSource
92
89
  .find('sys_activity', { $orderby: { timestamp: 'desc' }, $top: 20 })
93
90
  .catch((err) => {
94
91
  if (isMissingResource(err))
95
92
  activityUnavailableRef.current = true;
96
93
  return { data: [] };
97
94
  });
98
- try {
99
- const [presenceResult, activityResult] = await Promise.all([presenceP, activityP]);
100
- if (presenceResult.data?.length) {
101
- const users = presenceResult.data.filter((u) => typeof u.userId === 'string');
102
- if (users.length)
103
- setApiPresenceUsers(users);
104
- }
105
95
  if (activityResult.data?.length) {
106
96
  const items = activityResult.data.filter((a) => typeof a.type === 'string');
107
97
  if (items.length)
@@ -275,9 +265,23 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
275
265
  if (!dataSource)
276
266
  return;
277
267
  const now = new Date().toISOString();
268
+ const ids = unread.map(n => n.id);
269
+ // Prefer the bulk endpoint — one HTTP/auth/RLS round-trip instead of
270
+ // N parallel PATCHes, which previously caused noticeable hitching
271
+ // (Slack-style "mark all read" jank) on inboxes with 50+ unread.
272
+ // Fall back to the per-id loop on adapters that don't implement it.
273
+ const ds = dataSource;
274
+ if (typeof ds.bulkUpdate === 'function') {
275
+ try {
276
+ await ds.bulkUpdate('sys_notification', ids, { is_read: true, read_at: now });
277
+ return;
278
+ }
279
+ catch { /* fall through to per-id loop */ }
280
+ }
278
281
  await Promise.all(unread.map(n => dataSource.update('sys_notification', n.id, { is_read: true, read_at: now }).catch(() => { })));
279
282
  }, [dataSource, notifications]);
280
- const activeUsers = presenceUsers ?? apiPresenceUsers ?? EMPTY_PRESENCE_USERS;
283
+ const tenantPresence = useTenantPresence();
284
+ const activeUsers = presenceUsers ?? (tenantPresence.length > 0 ? tenantPresence : EMPTY_PRESENCE_USERS);
281
285
  const activeActivities = activities ?? apiActivities ?? [];
282
286
  const orgList = organizations ?? [];
283
287
  const hasOrgSection = isOrganizationsLoading || orgList.length > 0 || !!activeOrganization;