Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Create MetadataProvider that fetches all metadata (apps, objects, dashboards, reports, pages) from the ObjectStack API at runtime via ObjectStackAdapter. - Add MetadataProvider context with useMetadata() hook and refresh() - Add AdapterProvider context to lift adapter creation above routes - Replace static appConfig imports in App.tsx, DashboardView, ReportView, PageView, AppSidebar, and SearchResultsPage with useMetadata() - Keep objectstack.shared.ts only for MSW mock server (browser.ts, server.ts) - Update tests (BrowserSimulation, ConsoleApp, PageView) to mock providers Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the Console app to load metadata (apps/objects/dashboards/reports/pages) from the ObjectStack API at runtime rather than bundling example metadata via static imports, enabling the Console to correctly render metadata from the deployed project.
Changes:
- Introduces
AdapterProviderandMetadataProvidercontexts to share a single connected adapter instance and fetch metadata on mount. - Refactors several views/components to use
useMetadata()instead ofobjectstack.sharedimports. - Updates tests to mock the new providers (and keeps
objectstack.sharedusage for MSW seeding).
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/console/src/context/MetadataProvider.tsx | Adds runtime metadata fetching + useMetadata() hook and refresh(). |
| apps/console/src/context/AdapterProvider.tsx | Adds adapter creation/connection + useAdapter() hook. |
| apps/console/src/App.tsx | Wraps app routes with the new providers and replaces static config reads with useMetadata(). |
| apps/console/src/components/DashboardView.tsx | Loads dashboards via useMetadata() instead of bundled config. |
| apps/console/src/components/ReportView.tsx | Loads reports via useMetadata() instead of bundled config. |
| apps/console/src/components/PageView.tsx | Loads pages via useMetadata() instead of bundled config. |
| apps/console/src/components/AppSidebar.tsx | Loads apps list via useMetadata() instead of bundled config. |
| apps/console/src/components/SearchResultsPage.tsx | Loads apps/navigation via useMetadata() instead of bundled config. |
| apps/console/src/tests/PageView.test.tsx | Switches from mocking objectstack.shared to mocking useMetadata(). |
| apps/console/src/tests/ConsoleApp.test.tsx | Mocks both metadata + adapter providers/hooks for integration test rendering. |
| apps/console/src/tests/BrowserSimulation.test.tsx | Mocks both providers/hooks and updates adapter mock shape for new connection-state subscription usage. |
Comments suppressed due to low confidence (1)
apps/console/src/App.tsx:380
- RootRedirect returns when apps are empty even after loading is false. With API-driven metadata, this can happen on network/auth errors or a genuinely empty tenant, resulting in an infinite loading screen at '/'. Handle the empty/error case explicitly (e.g., show a "No apps configured" empty state or redirect to /login when unauthenticated).
// Redirect root to default app
function RootRedirect() {
const { apps, loading } = useMetadata();
const activeApps = apps.filter((a: any) => a.active !== false);
const defaultApp = activeApps.find((a: any) => a.isDefault === true) || activeApps[0];
if (loading) return <LoadingScreen />;
if (defaultApp) {
return <Navigate to={`/apps/${defaultApp.name}`} replace />;
}
return <LoadingScreen />;
}
| } catch (err) { | ||
| if (!cancelled) { | ||
| console.error('[Console] Failed to initialize:', err); | ||
| } | ||
| } |
There was a problem hiding this comment.
In the init() error path, the adapter remains null and the UI will sit on indefinitely with no user-visible failure state (only console.error). Consider storing an error in state/context and rendering an error screen with a retry action (or propagating the error so AppContent can handle connectionState='error').
|
|
||
| /** | ||
| * Creates an ObjectStackAdapter, connects to the API, then provides it to children. | ||
| * Shows nothing (returns null) until the adapter is ready. |
There was a problem hiding this comment.
The JSDoc says AdapterProvider "returns null" until the adapter is ready, but the component always renders the context provider and children (with a possibly null value). Either update the comment to match behavior, or actually gate rendering (e.g., return null / a loading UI) until an adapter exists.
| * Shows nothing (returns null) until the adapter is ready. | |
| * Until the adapter is ready, the context value will be null and consumers should handle this state (e.g., by showing a loading UI). |
| const [appsRes, objectsRes, dashboardsRes, reportsRes, pagesRes] = await Promise.all([ | ||
| client.meta.getItems('app').catch(() => ({ items: [] })), | ||
| client.meta.getItems('object').catch(() => ({ items: [] })), | ||
| client.meta.getItems('dashboard').catch(() => ({ items: [] })), | ||
| client.meta.getItems('report').catch(() => ({ items: [] })), | ||
| client.meta.getItems('page').catch(() => ({ items: [] })), | ||
| ]); | ||
|
|
||
| return { | ||
| apps: extractItems(appsRes), | ||
| objects: extractItems(objectsRes), | ||
| dashboards: extractItems(dashboardsRes), | ||
| reports: extractItems(reportsRes), | ||
| pages: extractItems(pagesRes), |
There was a problem hiding this comment.
fetchAllMetadata swallows all request failures via per-call .catch(() => ({ items: [] })), so MetadataState.error will almost never be set and consumers can't distinguish "no metadata" from "failed to load metadata". Use Promise.allSettled (or let rejections bubble) and set an aggregated error (and optionally log) when one or more categories fail.
| const [appsRes, objectsRes, dashboardsRes, reportsRes, pagesRes] = await Promise.all([ | |
| client.meta.getItems('app').catch(() => ({ items: [] })), | |
| client.meta.getItems('object').catch(() => ({ items: [] })), | |
| client.meta.getItems('dashboard').catch(() => ({ items: [] })), | |
| client.meta.getItems('report').catch(() => ({ items: [] })), | |
| client.meta.getItems('page').catch(() => ({ items: [] })), | |
| ]); | |
| return { | |
| apps: extractItems(appsRes), | |
| objects: extractItems(objectsRes), | |
| dashboards: extractItems(dashboardsRes), | |
| reports: extractItems(reportsRes), | |
| pages: extractItems(pagesRes), | |
| type CategoryKey = 'apps' | 'objects' | 'dashboards' | 'reports' | 'pages'; | |
| const categories: { key: CategoryKey; type: 'app' | 'object' | 'dashboard' | 'report' | 'page' }[] = [ | |
| { key: 'apps', type: 'app' }, | |
| { key: 'objects', type: 'object' }, | |
| { key: 'dashboards', type: 'dashboard' }, | |
| { key: 'reports', type: 'report' }, | |
| { key: 'pages', type: 'page' }, | |
| ]; | |
| const results = await Promise.allSettled( | |
| categories.map(category => client.meta.getItems(category.type)), | |
| ); | |
| const data: Record<CategoryKey, any[]> = { | |
| apps: [], | |
| objects: [], | |
| dashboards: [], | |
| reports: [], | |
| pages: [], | |
| }; | |
| const failedCategories: string[] = []; | |
| const errors: unknown[] = []; | |
| results.forEach((result, index) => { | |
| const { key } = categories[index]; | |
| if (result.status === 'fulfilled') { | |
| data[key] = extractItems(result.value); | |
| } else { | |
| // Preserve empty array for failed categories but record the failure. | |
| data[key] = []; | |
| failedCategories.push(key); | |
| errors.push(result.reason); | |
| } | |
| }); | |
| if (failedCategories.length > 0) { | |
| const message = `Failed to load metadata for categories: ${failedCategories.join(', ')}`; | |
| if (typeof console !== 'undefined' && typeof console.error === 'function') { | |
| console.error(message, errors); | |
| } | |
| throw new Error(message); | |
| } | |
| return { | |
| apps: data.apps, | |
| objects: data.objects, | |
| dashboards: data.dashboards, | |
| reports: data.reports, | |
| pages: data.pages, |
| export function MetadataProvider({ children, adapter }: MetadataProviderProps) { | ||
| const [state, setState] = useState<MetadataState>({ | ||
| apps: [], | ||
| objects: [], | ||
| dashboards: [], | ||
| reports: [], | ||
| pages: [], | ||
| loading: true, | ||
| error: null, | ||
| }); | ||
|
|
||
| const adapterRef = useRef(adapter); | ||
| adapterRef.current = adapter; | ||
|
|
||
| const refresh = useCallback(async () => { | ||
| setState(prev => ({ ...prev, loading: true, error: null })); | ||
| try { | ||
| const data = await fetchAllMetadata(adapterRef.current); | ||
| setState({ ...data, loading: false, error: null }); | ||
| } catch (err) { | ||
| setState(prev => ({ ...prev, loading: false, error: err instanceof Error ? err : new Error(String(err)) })); | ||
| } | ||
| }, []); |
There was a problem hiding this comment.
There are no direct tests for MetadataProvider's real behavior (initial fetch, partial failure handling, and refresh()). Current console tests mock useMetadata(), so regressions in the provider (e.g., loading/error transitions) won't be caught. Add a focused unit/integration test for this provider similar to ExpressionProvider tests.
Console bundles example metadata (CRM, Todo, KitchenSink) via static imports in
objectstack.shared.ts. When deployed to another ObjectStack project, it renders the bundled examples instead of that project's metadata.Changes
New context providers
MetadataProvider— fetches all metadata categories (app,object,dashboard,report,page) in parallel viaclient.meta.getItems()on mount. ExposesuseMetadata()hook withrefresh()for cache invalidation.AdapterProvider— liftsObjectStackAdaptercreation above routes so bothAppContentandRootRedirectshare a single connected instance. ExposesuseAdapter().Replaced static imports with API-driven metadata
App.tsx—ConnectedShellwraps routes withAdapterProvider+MetadataProviderDashboardView,ReportView,PageView,AppSidebar,SearchResultsPage— replacedappConfig.Xlookups withuseMetadata()objectstack.shared.tsscoped to MSW onlymocks/browser.tsandmocks/server.tsimport it now (to seed the mock server for dev/demo mode)Test updates
BrowserSimulation,ConsoleApp,PageViewtests mock bothMetadataProviderandAdapterProviderto provide test data without requiring a real API connection.💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.