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.
- package/package.json +1 -1
- package/templates/latest/.env.example +12 -2
- package/templates/latest/Agent.md +9 -756
- package/templates/latest/CLAUDE.md +43 -4
- package/templates/latest/app/app.tsx +63 -28
- package/templates/latest/app/components/error-boundary.tsx +178 -0
- package/templates/latest/app/constants/index.ts +7 -0
- package/templates/latest/app/contexts/app-context.tsx +20 -2
- package/templates/latest/app/single-spa.tsx +5 -0
- package/templates/latest/app/types/index.ts +29 -0
- package/templates/latest/app/utils/ag-grid-license.ts +8 -2
- package/templates/latest/package-lock.json +2784 -636
- package/templates/latest/package.json +13 -5
- package/templates/latest/pnpm-workspace.yaml +6 -0
- package/templates/latest/vite.config.ts +2 -2
|
@@ -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
|
|
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`
|
|
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()`
|
|
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 {
|
|
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 {
|
|
7
|
+
import { RouteErrorBoundary } from "~/components/error-boundary";
|
|
8
8
|
import RouteConfig from "./routes";
|
|
9
|
-
import type { MfeProps } from "~/types";
|
|
10
|
-
import {
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
+
envConfig.VITE_BLUECOPA_WORKSPACE_ID ||
|
|
37
42
|
DEFAULT_WORKSPACE_ID,
|
|
38
|
-
userId: props.userId || "",
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
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.",
|