create-bluecopa-react-app 1.0.42 → 1.0.44

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.
@@ -1,6 +1,6 @@
1
1
  # CLAUDE.md — React MFE Boilerplate
2
2
 
3
- This file provides guidance to Claude Code when working with the Bluecopa React MFE (Micro-Frontend) boilerplate.
3
+ This file provides guidance to Claude Code when working with the Bluecopa React MFE (Micro-Frontend) boilerplate. It is the **single source of truth** for agent guidance — `Agent.md` points here.
4
4
 
5
5
  ## Project Overview
6
6
 
@@ -33,18 +33,48 @@ interface MfeProps {
33
33
  accessToken?: string; // JWT auth token
34
34
  userId?: string; // Current user ID
35
35
  basename?: string; // Router basename
36
+ envVars?: EnvConfig; // Runtime env (workbook `storedEnvVariable`) — see below
37
+ schemaMap?: SchemaMap; // Column-name normalization (workbook `storedSchemaMap`)
36
38
  }
37
39
  ```
38
40
 
39
41
  Props are passed from the host app via single-spa `customProps` or `manualMount()`.
40
42
 
43
+ ### Runtime config: `envVars` (CRITICAL)
44
+
45
+ The host injects an `envVars` object built from the app's **Settings → Env Vars** panel
46
+ (persisted on the workbook as `storedEnvVariable`). This is the **only** way to configure a
47
+ deployed app without a rebuild — build-time `import.meta.env` is empty in the federated
48
+ `remoteEntry`. `app.tsx` merges them: `{ ...import.meta.env, ...props.envVars }`, host wins.
49
+ The merged object is exposed via `useAppContext().envConfig`. Add each of your app's keys (table
50
+ IDs, dataset IDs, …) **explicitly** to the `EnvConfig` interface in `app/types/index.ts` for
51
+ type-safe access — prefer this over the index signature, which is only a last-resort fallback for
52
+ keys you haven't typed yet.
53
+
54
+ ### Solution-context headers (Input Table V2)
55
+
56
+ `copaSetConfig` is applied **synchronously** in `app.tsx` (a `useState` initializer, so the first
57
+ SDK call already carries the right headers) and **re-applied via a `useEffect`** when the host
58
+ pushes new props/env after mount (e.g. a refreshed access token or updated `storedEnvVariable`).
59
+ It sets the `x-bluecopa-solution-*` headers from `envVars.VITE_BLUECOPA_SOLUTION_ID` /
60
+ `_SOLUTION_BRANCH` / `_SOLUTION_BRANCH_TYPE` — **required by the Input Table V2 SDK
61
+ (`copaInputTableDb`)**. Defaults: branch `main`, type `REMOTE`.
62
+
63
+ ### Error handling
64
+
65
+ `app/components/error-boundary.tsx` (`RouteErrorBoundary`) wraps the routed tree in `app.tsx`.
66
+ Without it, a render-phase throw unmounts the whole MFE and the host shows a blank div. It
67
+ auto-retries once (handles cache-cold races on first mount), then shows a visible error panel
68
+ with dev-only diagnostics.
69
+
41
70
  ### Key Files
42
71
 
43
72
  | File | Purpose |
44
73
  |------|---------|
45
74
  | `app/app.tsx` | Root component — QueryClient, ChartProvider, MFE config, AppProvider |
46
75
  | `app/app.css` | Tailwind v4 config + Dream Light theme tokens |
47
- | `app/contexts/app-context.tsx` | User, workspace settings, admin role context |
76
+ | `app/contexts/app-context.tsx` | User, workspace settings, admin role + `envConfig`/`schemaMap` |
77
+ | `app/components/error-boundary.tsx` | `RouteErrorBoundary` — wraps routes, auto-retry-once |
48
78
  | `app/routes/index.tsx` | Route definitions (all wrapped in AppLayout) |
49
79
  | `app/utils/utils.ts` | `cn()` utility with `copa` prefix support |
50
80
  | `app/utils/component-style.ts` | Unprefixed style tokens (source of truth) |
@@ -144,7 +174,7 @@ React Query hooks wrapping `@bluecopa/core`. All data fetching uses these hooks.
144
174
 
145
175
  ### Setup Flow
146
176
 
147
- 1. `copaSetConfig()` called in `app.tsx` useEffect with MFE props or env vars
177
+ 1. `copaSetConfig()` called **synchronously** in `app.tsx` (a `useState` initializer) from merged MFE props + env vars, then **re-applied via a `useEffect`** when those props/env change
148
178
  2. `QueryClientProvider` wraps the app (staleTime: 5min, retry: 1)
149
179
  3. Hooks become available in all child components
150
180
 
@@ -166,6 +196,15 @@ copaSetConfig({
166
196
  ```bash
167
197
  VITE_BLUECOPA_API_URL=https://develop.bluecopa.com/api/v1
168
198
  VITE_BLUECOPA_WORKSPACE_ID=prod
199
+ VITE_BLUECOPA_API_TOKEN= # standalone dev only
200
+ VITE_BLUECOPA_USER_ID=
201
+ VITE_AG_GRID_LICENSE_KEY=
202
+
203
+ # Solution context (Input Table V2). In a DEPLOYED app these arrive via the
204
+ # host's Settings → Env Vars panel (envVars), not this file.
205
+ VITE_BLUECOPA_SOLUTION_ID=
206
+ VITE_BLUECOPA_SOLUTION_BRANCH=main
207
+ VITE_BLUECOPA_SOLUTION_BRANCH_TYPE=REMOTE
169
208
  ```
170
209
 
171
210
  ### Available Hooks
@@ -756,4 +795,4 @@ Use `lucide-react` or `@tabler/icons-react` (both available).
756
795
  3. **`.mfe-root` not `:root`**: All CSS variables scoped to `.mfe-root` for MFE isolation
757
796
  4. **Breakpoints with prefix**: Must explicitly define `--breakpoint-*` vars in `@theme` (Tailwind v4 doesn't auto-include them with prefix)
758
797
  5. **ReactQueryDevtools**: Only rendered in dev mode (`import.meta.env.DEV`)
759
- 6. **Config before queries**: `copaSetConfig()` must complete before React Query hooks fire. The app gates rendering behind `isConfigured` state.
798
+ 6. **Config before queries**: `copaSetConfig()` runs synchronously in a `useState` initializer (before any child renders), so the first React Query hook call already has config no `isConfigured` render gate. It is also re-applied via a `useEffect` when props/env change (e.g. token refresh).
@@ -1,55 +1,90 @@
1
1
  import "./app.css";
2
- import { useState, useEffect } from "react";
2
+ import { useEffect, useMemo, useState } from "react";
3
3
  import { reactQuery, ReactQueryDevtools, copaSetConfig } from "@bluecopa/react";
4
4
  import { Toaster } from "~/components/ui/sonner";
5
5
  import { AppProvider } from "~/contexts/app-context";
6
6
  import { ChartProvider } from "~/components/charts";
7
- import { LoadingScreen } from "~/components/loading-screen";
7
+ import { RouteErrorBoundary } from "~/components/error-boundary";
8
8
  import RouteConfig from "./routes";
9
- import type { MfeProps } from "~/types";
10
- import { DEFAULT_API_BASE_URL, DEFAULT_WORKSPACE_ID, QUERY_DEFAULTS } from "~/constants";
9
+ import type { EnvConfig, MfeProps } from "~/types";
10
+ import {
11
+ DEFAULT_API_BASE_URL,
12
+ DEFAULT_SOLUTION_BRANCH,
13
+ DEFAULT_SOLUTION_BRANCH_TYPE,
14
+ DEFAULT_WORKSPACE_ID,
15
+ QUERY_DEFAULTS,
16
+ } from "~/constants";
11
17
  import { initAgGridLicense } from "~/utils/ag-grid-license";
12
18
 
13
19
  const { QueryClient, QueryClientProvider } = reactQuery;
14
20
 
15
21
  export default function App(props: MfeProps) {
16
- const [isConfigured, setIsConfigured] = useState(false);
17
- const [queryClient] = useState(
22
+ // Merge runtime env (host `envVars`) over build-time `import.meta.env`. Host
23
+ // values win so a deployed app can be reconfigured from the Settings → Env
24
+ // Vars panel without a rebuild. Recomputed if the host re-injects `envVars`.
25
+ const envConfig = useMemo<EnvConfig>(
18
26
  () =>
19
- new QueryClient({
20
- defaultOptions: {
21
- queries: { staleTime: QUERY_DEFAULTS.STALE_TIME, retry: QUERY_DEFAULTS.RETRY },
22
- },
23
- }),
27
+ ({ ...import.meta.env, ...(props.envVars ?? {}) }) as unknown as EnvConfig,
28
+ [props.envVars],
24
29
  );
25
30
 
26
- useEffect(() => {
27
- initAgGridLicense();
28
- copaSetConfig({
31
+ // SDK config: top-level mount props win over env. Solution-context headers
32
+ // (`x-bluecopa-solution-*`) are required by the Input Table V2 SDK and only
33
+ // ever arrive via the host's `envVars`.
34
+ const sdkConfig = useMemo(
35
+ () => ({
29
36
  apiBaseUrl:
30
- props.apiBaseUrl ||
31
- import.meta.env.VITE_BLUECOPA_API_URL ||
32
- DEFAULT_API_BASE_URL,
33
- accessToken: props.accessToken || "",
37
+ props.apiBaseUrl || envConfig.VITE_BLUECOPA_API_URL || DEFAULT_API_BASE_URL,
38
+ accessToken: props.accessToken || envConfig.VITE_BLUECOPA_API_TOKEN || "",
34
39
  workspaceId:
35
40
  props.workspaceId ||
36
- import.meta.env.VITE_BLUECOPA_WORKSPACE_ID ||
41
+ envConfig.VITE_BLUECOPA_WORKSPACE_ID ||
37
42
  DEFAULT_WORKSPACE_ID,
38
- userId: props.userId || "",
39
- });
40
- setIsConfigured(true);
41
- }, [props.apiBaseUrl, props.accessToken, props.workspaceId, props.userId]);
43
+ userId: props.userId || envConfig.VITE_BLUECOPA_USER_ID || "",
44
+ solutionId: envConfig.VITE_BLUECOPA_SOLUTION_ID || undefined,
45
+ solutionBranch:
46
+ envConfig.VITE_BLUECOPA_SOLUTION_BRANCH || DEFAULT_SOLUTION_BRANCH,
47
+ solutionBranchType:
48
+ envConfig.VITE_BLUECOPA_SOLUTION_BRANCH_TYPE ||
49
+ DEFAULT_SOLUTION_BRANCH_TYPE,
50
+ }),
51
+ [props.apiBaseUrl, props.accessToken, props.workspaceId, props.userId, envConfig],
52
+ );
42
53
 
43
- if (!isConfigured) {
44
- return <LoadingScreen message="Initializing..." />;
45
- }
54
+ // Configure the SDK SYNCHRONOUSLY before any child renders, so hooks that read
55
+ // userId/solutionId from the SDK on their first call (useUser, Input Table V2)
56
+ // see the right values — a useEffect alone would render children once with
57
+ // empty config. This initializer runs exactly once on mount.
58
+ useState(() => {
59
+ copaSetConfig(sdkConfig);
60
+ initAgGridLicense(envConfig.VITE_AG_GRID_LICENSE_KEY);
61
+ return null;
62
+ });
63
+
64
+ // Re-apply when the host pushes new props/env after mount (e.g. a refreshed
65
+ // access token or updated solution headers). `copaSetConfig` shallow-merges
66
+ // into a singleton, so re-applying is idempotent.
67
+ useEffect(() => {
68
+ copaSetConfig(sdkConfig);
69
+ }, [sdkConfig]);
70
+
71
+ const [queryClient] = useState(
72
+ () =>
73
+ new QueryClient({
74
+ defaultOptions: {
75
+ queries: { staleTime: QUERY_DEFAULTS.STALE_TIME, retry: QUERY_DEFAULTS.RETRY },
76
+ },
77
+ }),
78
+ );
46
79
 
47
80
  return (
48
81
  <QueryClientProvider client={queryClient}>
49
82
  <div className="mfe-root copa:min-h-svh copa:animate-in copa:fade-in copa:duration-300">
50
83
  <ChartProvider>
51
- <AppProvider>
52
- <RouteConfig />
84
+ <AppProvider envConfig={envConfig} schemaMap={props.schemaMap}>
85
+ <RouteErrorBoundary>
86
+ <RouteConfig />
87
+ </RouteErrorBoundary>
53
88
  <Toaster />
54
89
  </AppProvider>
55
90
  </ChartProvider>
@@ -0,0 +1,178 @@
1
+ import {
2
+ Component,
3
+ Fragment,
4
+ type ErrorInfo,
5
+ type ReactNode,
6
+ } from "react";
7
+
8
+ interface Props {
9
+ children: ReactNode;
10
+ }
11
+
12
+ interface State {
13
+ error: Error | null;
14
+ info: ErrorInfo | null;
15
+ /** Number of times we've remounted children after a caught error. Used as a
16
+ * React `key` on the children fragment so a bumped count forces a fresh
17
+ * unmount/remount instead of a re-render. */
18
+ retryKey: number;
19
+ /** Whether the auto-retry slot has been consumed. We auto-retry exactly once
20
+ * per boundary lifetime (until a manual reset via "Try again"), then surface
21
+ * the error UI on a second consecutive throw so we don't loop forever. */
22
+ autoRetryUsed: boolean;
23
+ }
24
+
25
+ const AUTO_RETRY_DELAY_MS = 100;
26
+
27
+ /**
28
+ * Route-level error boundary. Without this, a render-phase throw anywhere
29
+ * inside the routed tree unmounts the whole MFE and the single-spa host
30
+ * renders a blank div — the classic "white screen, no console error" symptom.
31
+ *
32
+ * The boundary auto-retries once on the first error. This catches the common
33
+ * "cache-cold race on initial mount" pattern: the first mount throws because a
34
+ * dependent dataset is still empty, but that failed mount triggered the fetch
35
+ * that warms the cache, so the remount succeeds against populated data. The
36
+ * 100ms delay lets any in-flight `setState` from the failed mount flush and
37
+ * the dependent fetches start resolving. If the retry also fails, we stop and
38
+ * show the visible error panel instead of looping forever.
39
+ */
40
+ export class RouteErrorBoundary extends Component<Props, State> {
41
+ state: State = {
42
+ error: null,
43
+ info: null,
44
+ retryKey: 0,
45
+ autoRetryUsed: false,
46
+ };
47
+
48
+ private retryTimer: number | null = null;
49
+
50
+ static getDerivedStateFromError(error: Error): Partial<State> {
51
+ return { error };
52
+ }
53
+
54
+ componentDidCatch(error: Error, info: ErrorInfo): void {
55
+ // Prefix is intentional — makes the error easy to spot in the host console
56
+ // where other MFEs may also be logging.
57
+ console.error("[ROUTE BOUNDARY]", error, info);
58
+ this.setState({ info });
59
+
60
+ if (this.state.autoRetryUsed) {
61
+ // Already auto-retried once — don't loop. The user will see the visible
62
+ // error panel below (and can still reset manually via "Try again").
63
+ return;
64
+ }
65
+
66
+ // Schedule the remount on a macrotask so React finishes commit on the
67
+ // failed mount before we clear the error and bump the key. A synchronous
68
+ // setState here would be coalesced into the same batch as the error-derived
69
+ // state update and the unmount/remount cycle wouldn't happen.
70
+ this.retryTimer = window.setTimeout(() => {
71
+ this.retryTimer = null;
72
+ this.setState((s) => ({
73
+ error: null,
74
+ info: null,
75
+ retryKey: s.retryKey + 1,
76
+ autoRetryUsed: true,
77
+ }));
78
+ }, AUTO_RETRY_DELAY_MS);
79
+ }
80
+
81
+ componentWillUnmount(): void {
82
+ if (this.retryTimer !== null) {
83
+ window.clearTimeout(this.retryTimer);
84
+ this.retryTimer = null;
85
+ }
86
+ }
87
+
88
+ handleReset = (): void => {
89
+ this.setState((s) => ({
90
+ error: null,
91
+ info: null,
92
+ retryKey: s.retryKey + 1,
93
+ autoRetryUsed: false,
94
+ }));
95
+ };
96
+
97
+ render(): ReactNode {
98
+ if (this.state.error) {
99
+ const message = this.state.error.message || String(this.state.error);
100
+ const stack = this.state.error.stack ?? "";
101
+ const componentStack = this.state.info?.componentStack ?? "";
102
+ // Stack + component stack are leaky in prod (file paths, internal symbol
103
+ // names) and aren't useful to end users — show them in dev only. The full
104
+ // error is still logged via `componentDidCatch` for any deploy.
105
+ const showDiagnostics = import.meta.env.DEV;
106
+
107
+ return (
108
+ <div className="copa:flex copa:min-h-svh copa:items-start copa:justify-center copa:bg-background copa:p-6">
109
+ <div className="copa:w-full copa:max-w-3xl copa:space-y-4">
110
+ <div>
111
+ <h1 className="copa:text-xl copa:font-semibold copa:text-foreground">
112
+ Something went wrong on this page
113
+ </h1>
114
+ <p className="copa:mt-1 copa:text-sm copa:text-muted-foreground">
115
+ The page errored while rendering. Details are below — share this
116
+ with engineering if it keeps happening.
117
+ </p>
118
+ </div>
119
+ <div className="copa:rounded-md copa:border copa:border-destructive/40 copa:bg-destructive/5 copa:p-4">
120
+ <p className="copa:text-sm copa:font-medium copa:text-destructive">
121
+ {message}
122
+ </p>
123
+ </div>
124
+ {showDiagnostics && (
125
+ <details className="copa:rounded-md copa:border copa:bg-muted/30 copa:p-4 copa:text-xs">
126
+ <summary className="copa:cursor-pointer copa:text-sm copa:font-medium">
127
+ Stack trace
128
+ </summary>
129
+ <pre className="copa:mt-2 copa:overflow-auto copa:whitespace-pre-wrap">
130
+ {stack}
131
+ </pre>
132
+ </details>
133
+ )}
134
+ {showDiagnostics && componentStack && (
135
+ <details className="copa:rounded-md copa:border copa:bg-muted/30 copa:p-4 copa:text-xs">
136
+ <summary className="copa:cursor-pointer copa:text-sm copa:font-medium">
137
+ Component stack
138
+ </summary>
139
+ <pre className="copa:mt-2 copa:overflow-auto copa:whitespace-pre-wrap">
140
+ {componentStack}
141
+ </pre>
142
+ </details>
143
+ )}
144
+ <div className="copa:flex copa:gap-2">
145
+ <button
146
+ type="button"
147
+ onClick={this.handleReset}
148
+ className="copa:inline-flex copa:items-center copa:rounded-md copa:border copa:border-input copa:bg-background copa:px-4 copa:py-2 copa:text-sm copa:font-medium copa:hover:bg-accent"
149
+ >
150
+ Try again
151
+ </button>
152
+ <button
153
+ type="button"
154
+ onClick={() => {
155
+ // Full reload of the CURRENT url — preserves the host-provided
156
+ // MFE basename. Navigating to "/" would escape the mounted app
157
+ // to the host root.
158
+ window.location.reload();
159
+ }}
160
+ className="copa:inline-flex copa:items-center copa:rounded-md copa:bg-primary copa:px-4 copa:py-2 copa:text-sm copa:font-medium copa:text-primary-foreground copa:hover:bg-primary/90"
161
+ >
162
+ Reload page
163
+ </button>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ );
168
+ }
169
+
170
+ // `key` is the lynchpin of the auto-retry: bumping it on retry causes React
171
+ // to fully unmount the old subtree and mount a fresh one, resetting every
172
+ // hook's local state. Without the key, React would re-render in place with
173
+ // the same hook state that just threw, and we'd loop on the same error.
174
+ return (
175
+ <Fragment key={this.state.retryKey}>{this.props.children}</Fragment>
176
+ );
177
+ }
178
+ }
@@ -11,6 +11,13 @@ export const ROUTES = {
11
11
  export const DEFAULT_API_BASE_URL = "http://localhost:3000/api/v1";
12
12
  export const DEFAULT_WORKSPACE_ID = "prod";
13
13
 
14
+ // ─── Solution Context Defaults ──────────────────────────────────────────────
15
+ // Used for the `x-bluecopa-solution-*` SDK headers (Input Table V2 / deployed
16
+ // solutions). There is no guaranteed branch named `local` — the default branch
17
+ // on a solution is `main`/`REMOTE`.
18
+ export const DEFAULT_SOLUTION_BRANCH = "main";
19
+ export const DEFAULT_SOLUTION_BRANCH_TYPE = "REMOTE";
20
+
14
21
  /** React Query defaults */
15
22
  export const QUERY_DEFAULTS = {
16
23
  STALE_TIME: 5 * 60 * 1000, // 5 minutes
@@ -1,6 +1,6 @@
1
1
  import { createContext, useContext, useMemo, useState, type ReactNode } from "react";
2
2
  import { useUser, useGetAllUsers } from "@bluecopa/react";
3
- import type { AppUser, WorkspaceDataSettings } from "~/types";
3
+ import type { AppUser, EnvConfig, SchemaMap, WorkspaceDataSettings } from "~/types";
4
4
  import { DEFAULT_WORKSPACE_SETTINGS } from "~/constants";
5
5
 
6
6
  interface AppContextValue {
@@ -17,6 +17,10 @@ interface AppContextValue {
17
17
  /** Workspace data settings (currency, timezone, date format, fiscal year, number settings, week start day) */
18
18
  currentWorkspaceSettings: WorkspaceDataSettings;
19
19
  setCurrency: (currency: string) => void;
20
+ /** Merged runtime env config (host `envVars` over build-time `import.meta.env`). */
21
+ envConfig: EnvConfig;
22
+ /** Column-name normalization map injected by the host (workbook `storedSchemaMap`). */
23
+ schemaMap: SchemaMap;
20
24
  }
21
25
 
22
26
  const AppContext = createContext<AppContextValue | undefined>(undefined);
@@ -65,9 +69,19 @@ function getWorkspaceDataSettings(workspace: unknown): WorkspaceDataSettings {
65
69
 
66
70
  export type AppProviderProps = {
67
71
  children: ReactNode;
72
+ /** Merged runtime env config. Defaults to build-time `import.meta.env`. */
73
+ envConfig?: EnvConfig;
74
+ /** Host-injected schema map. Defaults to empty. */
75
+ schemaMap?: SchemaMap;
68
76
  };
69
77
 
70
- export function AppProvider({ children }: AppProviderProps) {
78
+ /** Stable empty fallback so the context-value memo isn't invalidated every render. */
79
+ const EMPTY_SCHEMA_MAP: SchemaMap = {};
80
+
81
+ export function AppProvider({ children, envConfig, schemaMap }: AppProviderProps) {
82
+ const resolvedEnvConfig = (envConfig ??
83
+ (import.meta.env as unknown as EnvConfig)) as EnvConfig;
84
+ const resolvedSchemaMap = schemaMap ?? EMPTY_SCHEMA_MAP;
71
85
  const [currencyOverride, setCurrencyOverride] = useState<string | null>(null);
72
86
 
73
87
  const {
@@ -169,6 +183,8 @@ export function AppProvider({ children }: AppProviderProps) {
169
183
  usersError: usersError ?? null,
170
184
  currentWorkspaceSettings,
171
185
  setCurrency: setCurrencyOverride,
186
+ envConfig: resolvedEnvConfig,
187
+ schemaMap: resolvedSchemaMap,
172
188
  }),
173
189
  [
174
190
  userWithDetails,
@@ -184,6 +200,8 @@ export function AppProvider({ children }: AppProviderProps) {
184
200
  usersLoading,
185
201
  usersError,
186
202
  currentWorkspaceSettings,
203
+ resolvedEnvConfig,
204
+ resolvedSchemaMap,
187
205
  ]
188
206
  );
189
207
 
@@ -65,6 +65,11 @@ export async function mount(props: MountProps): Promise<void> {
65
65
  workspaceId: props.workspaceId,
66
66
  apiBaseUrl: props.apiBaseUrl,
67
67
  userId: props.userId,
68
+ // Runtime config + schema map injected by the host. Forwarding these
69
+ // is what lets a deployed app read its Settings → Env Vars config and
70
+ // normalize API column names at runtime.
71
+ envVars: props.envVars,
72
+ schemaMap: props.schemaMap,
68
73
  }}
69
74
  />,
70
75
  );
@@ -1,3 +1,28 @@
1
+ /** Column-name normalization map injected by the host (workbook `storedSchemaMap`).
2
+ * Keyed by env-var name → { backendColumn: frontendColumn }. */
3
+ export type SchemaMap = Record<string, Record<string, string>>;
4
+
5
+ /**
6
+ * Runtime environment config. The host injects these via the `envVars` prop
7
+ * (the app's Settings → Env Vars panel, persisted on the workbook). At
8
+ * build-time / standalone dev they come from `import.meta.env`. The index
9
+ * signature lets app-specific vars (table IDs, dataset IDs, …) flow through
10
+ * without being listed here.
11
+ */
12
+ export interface EnvConfig {
13
+ VITE_BLUECOPA_API_URL?: string;
14
+ VITE_BLUECOPA_WORKSPACE_ID?: string;
15
+ VITE_BLUECOPA_API_TOKEN?: string;
16
+ VITE_BLUECOPA_USER_ID?: string;
17
+ VITE_AG_GRID_LICENSE_KEY?: string;
18
+ /** Required by the Input Table V2 SDK (`copaInputTableDb`). Flows through to
19
+ * the `x-bluecopa-solution-*` request headers. */
20
+ VITE_BLUECOPA_SOLUTION_ID?: string;
21
+ VITE_BLUECOPA_SOLUTION_BRANCH?: string;
22
+ VITE_BLUECOPA_SOLUTION_BRANCH_TYPE?: string;
23
+ [key: string]: string | undefined;
24
+ }
25
+
1
26
  /** Props passed to the MFE root component from the host app (single-spa customProps or manualMount). */
2
27
  export interface MfeProps {
3
28
  apiBaseUrl?: string;
@@ -5,6 +30,10 @@ export interface MfeProps {
5
30
  accessToken?: string;
6
31
  userId?: string;
7
32
  basename?: string;
33
+ /** Runtime env vars injected by the host (workbook `storedEnvVariable`). */
34
+ envVars?: EnvConfig;
35
+ /** Column-name normalization map injected by the host (workbook `storedSchemaMap`). */
36
+ schemaMap?: SchemaMap;
8
37
  }
9
38
 
10
39
  /** Normalized user object provided by AppContext. */
@@ -1,7 +1,13 @@
1
1
  import { LicenseManager } from "ag-grid-enterprise";
2
2
 
3
- export function initAgGridLicense() {
4
- const key = import.meta.env.VITE_AG_GRID_LICENSE_KEY;
3
+ /**
4
+ * Set the AG Grid Enterprise license key. Pass the key explicitly (e.g. from
5
+ * the host-injected runtime env config) so a deployed MFE can be licensed from
6
+ * the Settings → Env Vars panel. Falls back to the build-time env var for
7
+ * standalone dev.
8
+ */
9
+ export function initAgGridLicense(licenseKey?: string) {
10
+ const key = licenseKey || import.meta.env.VITE_AG_GRID_LICENSE_KEY;
5
11
  if (!key) {
6
12
  console.warn(
7
13
  "[AG Grid] No license key found (VITE_AG_GRID_LICENSE_KEY). Enterprise features will show watermarks.",