@equinor/fusion-framework-react-app 14.1.0 → 14.1.2
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/dist/esm/version.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/version.d.ts +1 -1
- package/package.json +33 -30
- package/CHANGELOG.md +0 -2785
- package/docs/ag-grid.md +0 -70
- package/docs/analytics.md +0 -122
- package/docs/app.md +0 -148
- package/docs/apploader.md +0 -110
- package/docs/bookmark.md +0 -108
- package/docs/context.md +0 -88
- package/docs/feature-flag.md +0 -119
- package/docs/framework.md +0 -93
- package/docs/help-center.md +0 -88
- package/docs/http.md +0 -118
- package/docs/msal.md +0 -291
- package/docs/navigation.md +0 -80
- package/docs/routing.md +0 -86
- package/docs/settings.md +0 -139
- package/src/__tests__/Apploader.test.tsx +0 -51
- package/src/__tests__/fixtures/apploader-child-script.ts +0 -9
- package/src/__tests__/render-app.test.tsx +0 -113
- package/src/__tests__/testApp.test.tsx +0 -76
- package/src/__tests__/useAccessToken.test.tsx +0 -51
- package/src/__tests__/useAppSetting.test.tsx +0 -133
- package/src/__tests__/useAppSettings.test.tsx +0 -147
- package/src/__tests__/useCurrentAccount.test.tsx +0 -32
- package/src/__tests__/useCurrentBookmark.test.tsx +0 -108
- package/src/__tests__/useCurrentContext.test.tsx +0 -72
- package/src/__tests__/useFeature.test.tsx +0 -104
- package/src/__tests__/useHelpCenter.test.tsx +0 -64
- package/src/__tests__/useStateSyncEvents.test.ts +0 -58
- package/src/__tests__/useToken.test.tsx +0 -71
- package/src/__tests__/useTrackFeature.test.tsx +0 -83
- package/src/ag-grid/community.ts +0 -11
- package/src/ag-grid/enterprise.ts +0 -11
- package/src/ag-grid/react.ts +0 -11
- package/src/ag-grid/testing.ts +0 -19
- package/src/ag-grid/theme.ts +0 -17
- package/src/ag-grid/useTheme.ts +0 -21
- package/src/analytics/README.md +0 -148
- package/src/analytics/index.ts +0 -9
- package/src/analytics/useTrackFeature.ts +0 -73
- package/src/apploader/Apploader.tsx +0 -50
- package/src/apploader/README.md +0 -81
- package/src/apploader/index.ts +0 -10
- package/src/apploader/useApploader.ts +0 -107
- package/src/bookmark/index.ts +0 -20
- package/src/bookmark/useBookmark.ts +0 -25
- package/src/bookmark/useCurrentBookmark.ts +0 -54
- package/src/context/index.ts +0 -14
- package/src/context/useContextProvider.ts +0 -12
- package/src/context/useCurrentContext.ts +0 -20
- package/src/create-component.tsx +0 -120
- package/src/create-legacy-app.tsx +0 -51
- package/src/feature-flag/README.md +0 -20
- package/src/feature-flag/enable-feature-flag.ts +0 -79
- package/src/feature-flag/index.ts +0 -17
- package/src/feature-flag/useFeature.ts +0 -90
- package/src/framework/index.ts +0 -14
- package/src/framework/useFrameworkCurrentContext.ts +0 -15
- package/src/help-center/README.md +0 -21
- package/src/help-center/event-name.ts +0 -1
- package/src/help-center/index.ts +0 -11
- package/src/help-center/useHelpCenter.ts +0 -162
- package/src/http/index.ts +0 -10
- package/src/http/selectors.ts +0 -9
- package/src/index.ts +0 -46
- package/src/make-component.tsx +0 -102
- package/src/msal/index.ts +0 -16
- package/src/msal/useAccessToken.ts +0 -26
- package/src/msal/useCurrentAccount.ts +0 -21
- package/src/msal/useToken.ts +0 -63
- package/src/navigation/index.ts +0 -10
- package/src/navigation/useNavigationModule.ts +0 -10
- package/src/navigation/useRouter.ts +0 -35
- package/src/render-app.ts +0 -35
- package/src/render-component.tsx +0 -55
- package/src/routing/index.ts +0 -33
- package/src/settings/README.md +0 -123
- package/src/settings/index.ts +0 -12
- package/src/settings/useAppSetting.ts +0 -113
- package/src/settings/useAppSettings.ts +0 -103
- package/src/settings/useAppSettingsStatus.ts +0 -52
- package/src/state/index.ts +0 -12
- package/src/state/useAppState.ts +0 -299
- package/src/state/useStateSyncEvents.ts +0 -59
- package/src/useAppEnvironmentVariables.ts +0 -59
- package/src/useAppModule.ts +0 -45
- package/src/useAppModules.ts +0 -21
- package/src/version.ts +0 -2
- package/tsconfig.json +0 -54
- package/vitest.config.ts +0 -9
package/docs/msal.md
DELETED
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
# MSAL Authentication
|
|
2
|
-
|
|
3
|
-
This package includes React hooks for Microsoft authentication using MSAL v4.
|
|
4
|
-
|
|
5
|
-
> [!CAUTION]
|
|
6
|
-
> **Applications should NOT configure the MSAL module themselves.**
|
|
7
|
-
>
|
|
8
|
-
> The MSAL module **must be configured by the host/portal application**, not by individual apps. This is required for module hoisting, which allows sharing authentication state across all applications in a portal.
|
|
9
|
-
>
|
|
10
|
-
> - ✅ **Host/Portal:** Configure MSAL using `enableMSAL()` in the portal's configuration
|
|
11
|
-
> - ❌ **App:** Do NOT call `enableMSAL()` or configure the auth module
|
|
12
|
-
> - ✅ **App:** Just use the hooks to access the already-configured MSAL module
|
|
13
|
-
|
|
14
|
-
> [!IMPORTANT]
|
|
15
|
-
> `@equinor/fusion-framework-module-msal` must be installed to make MSAL hooks available
|
|
16
|
-
|
|
17
|
-
## Overview
|
|
18
|
-
|
|
19
|
-
The MSAL authentication hooks provide a simple way to integrate Microsoft authentication into your React applications. These hooks are built on top of `@equinor/fusion-framework-module-msal` and provide React-friendly access to authentication state and token acquisition.
|
|
20
|
-
|
|
21
|
-
Since the MSAL module is configured by your host application, you don't need to worry about configuration—just import and use the hooks!
|
|
22
|
-
|
|
23
|
-
The hooks use a simplified API that internally handles the MSAL v4 format, so you can use the simple `{ scopes: string[] }` format without worrying about the v4 nested request structure.
|
|
24
|
-
|
|
25
|
-
## Available Hooks
|
|
26
|
-
|
|
27
|
-
### useCurrentAccount
|
|
28
|
-
|
|
29
|
-
Returns the current authenticated user account information.
|
|
30
|
-
|
|
31
|
-
**Signature:**
|
|
32
|
-
```typescript
|
|
33
|
-
useCurrentAccount(): AccountInfo | undefined
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
**Example:**
|
|
37
|
-
```tsx
|
|
38
|
-
import { useCurrentAccount } from '@equinor/fusion-framework-react-app/msal';
|
|
39
|
-
|
|
40
|
-
const UserProfile = () => {
|
|
41
|
-
const account = useCurrentAccount();
|
|
42
|
-
|
|
43
|
-
if (!account) {
|
|
44
|
-
return <span>Not authenticated</span>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<div>
|
|
49
|
-
<p>Username: {account.username}</p>
|
|
50
|
-
<p>Name: {account.name}</p>
|
|
51
|
-
<p>Tenant ID: {account.tenantId}</p>
|
|
52
|
-
</div>
|
|
53
|
-
);
|
|
54
|
-
};
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
**Returns:** `AccountInfo | undefined`
|
|
58
|
-
|
|
59
|
-
### useAccessToken
|
|
60
|
-
|
|
61
|
-
Returns just the access token string for making authenticated API calls.
|
|
62
|
-
|
|
63
|
-
**Signature:**
|
|
64
|
-
```typescript
|
|
65
|
-
useAccessToken(req: { scopes: string[] }): { token?: string; pending: boolean; error: unknown }
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
**Example:**
|
|
69
|
-
```tsx
|
|
70
|
-
import { useAccessToken } from '@equinor/fusion-framework-react-app/msal';
|
|
71
|
-
import { useMemo } from 'react';
|
|
72
|
-
|
|
73
|
-
const ProtectedComponent = () => {
|
|
74
|
-
const { token, pending, error } = useAccessToken(
|
|
75
|
-
useMemo(() => ({ scopes: ['User.Read'] }), [])
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
if (pending) return <span>Loading token...</span>;
|
|
79
|
-
if (error) return <span>Error: {String(error)}</span>;
|
|
80
|
-
|
|
81
|
-
return token ? <span>Token acquired</span> : <span>No token</span>;
|
|
82
|
-
};
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
**Parameters:**
|
|
86
|
-
- `req`: Object with `scopes` property - Array of OAuth scopes to request
|
|
87
|
-
|
|
88
|
-
**Returns:**
|
|
89
|
-
- `token?: string` - The access token string if available
|
|
90
|
-
- `pending: boolean` - Whether the token request is in progress
|
|
91
|
-
- `error: unknown` - Any error that occurred during token acquisition
|
|
92
|
-
|
|
93
|
-
### useToken
|
|
94
|
-
|
|
95
|
-
Returns the full authentication result object with complete token information.
|
|
96
|
-
|
|
97
|
-
**Signature:**
|
|
98
|
-
```typescript
|
|
99
|
-
useToken(req: { scopes: string[] }): { token?: AuthenticationResult; pending: boolean; error: unknown }
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
**Example:**
|
|
103
|
-
```tsx
|
|
104
|
-
import { useToken } from '@equinor/fusion-framework-react-app/msal';
|
|
105
|
-
import { useMemo } from 'react';
|
|
106
|
-
|
|
107
|
-
const TokenInfo = () => {
|
|
108
|
-
const { token, pending, error } = useToken(
|
|
109
|
-
useMemo(() => ({ scopes: ['User.Read', 'api.read'] }), [])
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
if (pending) return <span>Loading...</span>;
|
|
113
|
-
if (error) return <span>Error: {String(error)}</span>;
|
|
114
|
-
if (!token) return <span>No token</span>;
|
|
115
|
-
|
|
116
|
-
return (
|
|
117
|
-
<div>
|
|
118
|
-
<p>Access Token: {token.accessToken.substring(0, 20)}...</p>
|
|
119
|
-
<p>Expires: {token.expiresOn?.toLocaleString() ?? 'unknown'}</p>
|
|
120
|
-
<p>Token Type: {token.tokenType}</p>
|
|
121
|
-
<p>Scope: {token.scopes.join(', ')}</p>
|
|
122
|
-
</div>
|
|
123
|
-
);
|
|
124
|
-
};
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
**Parameters:**
|
|
128
|
-
- `req`: Object with `scopes` property - Array of OAuth scopes to request
|
|
129
|
-
|
|
130
|
-
**Returns:**
|
|
131
|
-
- `token?: AuthenticationResult` - Full authentication result containing accessToken, account info, expiresOn, etc.
|
|
132
|
-
- `pending: boolean` - Whether the token request is in progress
|
|
133
|
-
- `error: unknown` - Any error that occurred during token acquisition
|
|
134
|
-
|
|
135
|
-
**Note:** The `AuthenticationResult` type includes:
|
|
136
|
-
- `accessToken: string`
|
|
137
|
-
- `account: AccountInfo | null`
|
|
138
|
-
- `expiresOn: Date | null`
|
|
139
|
-
- `tokenType: string`
|
|
140
|
-
- `scopes: string[]`
|
|
141
|
-
- And more...
|
|
142
|
-
|
|
143
|
-
## How It Works
|
|
144
|
-
|
|
145
|
-
Internally, these hooks call the MSAL provider's methods which support both legacy and modern formats:
|
|
146
|
-
|
|
147
|
-
```typescript
|
|
148
|
-
// The hook receives simple format:
|
|
149
|
-
useToken({ scopes: ['User.Read'] })
|
|
150
|
-
|
|
151
|
-
// Internally converts to MSAL v4 format:
|
|
152
|
-
msalProvider.acquireToken({ request: { scopes: ['User.Read'] } })
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
This means you can use the simpler API without dealing with the v4 nested structure, while still benefiting from MSAL v4 features under the hood.
|
|
156
|
-
|
|
157
|
-
## Complete Example
|
|
158
|
-
|
|
159
|
-
```tsx
|
|
160
|
-
import { useCurrentAccount, useAccessToken } from '@equinor/fusion-framework-react-app/msal';
|
|
161
|
-
import { useMemo, useState } from 'react';
|
|
162
|
-
|
|
163
|
-
const App = () => {
|
|
164
|
-
const account = useCurrentAccount();
|
|
165
|
-
|
|
166
|
-
// Get scopes from somewhere (e.g., service discovery)
|
|
167
|
-
const [scopes] = useState(['User.Read', 'api.read']);
|
|
168
|
-
|
|
169
|
-
const { token, pending, error } = useAccessToken(
|
|
170
|
-
useMemo(() => ({ scopes }), [scopes])
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
return (
|
|
174
|
-
<div>
|
|
175
|
-
{!account && <p>Please log in</p>}
|
|
176
|
-
|
|
177
|
-
{account && (
|
|
178
|
-
<>
|
|
179
|
-
<h1>Welcome, {account.name}!</h1>
|
|
180
|
-
<p>Username: {account.username}</p>
|
|
181
|
-
|
|
182
|
-
{pending && <p>Loading token...</p>}
|
|
183
|
-
{error && <p>Error: {String(error)}</p>}
|
|
184
|
-
{token && (
|
|
185
|
-
<div>
|
|
186
|
-
<p>Token acquired successfully</p>
|
|
187
|
-
<pre>{token.substring(0, 50)}...</pre>
|
|
188
|
-
</div>
|
|
189
|
-
)}
|
|
190
|
-
</>
|
|
191
|
-
)}
|
|
192
|
-
</div>
|
|
193
|
-
);
|
|
194
|
-
};
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
## Advanced Usage
|
|
198
|
-
|
|
199
|
-
### Dynamic Scopes
|
|
200
|
-
|
|
201
|
-
```tsx
|
|
202
|
-
const Component = () => {
|
|
203
|
-
const [scopes, setScopes] = useState(['User.Read']);
|
|
204
|
-
|
|
205
|
-
const { token, pending } = useAccessToken(
|
|
206
|
-
useMemo(() => ({ scopes }), [scopes])
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
return (
|
|
210
|
-
<div>
|
|
211
|
-
<button onClick={() => setScopes(['User.Read', 'api.write'])}>
|
|
212
|
-
Request More Permissions
|
|
213
|
-
</button>
|
|
214
|
-
{token && <p>Token ready</p>}
|
|
215
|
-
</div>
|
|
216
|
-
);
|
|
217
|
-
};
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
### Error Handling
|
|
221
|
-
|
|
222
|
-
```tsx
|
|
223
|
-
const Component = () => {
|
|
224
|
-
const { token, error, pending } = useAccessToken(
|
|
225
|
-
useMemo(() => ({ scopes: ['User.Read'] }), [])
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
useEffect(() => {
|
|
229
|
-
if (error) {
|
|
230
|
-
console.error('Token acquisition failed:', error);
|
|
231
|
-
// Handle error appropriately
|
|
232
|
-
}
|
|
233
|
-
}, [error]);
|
|
234
|
-
|
|
235
|
-
return pending ? <Loading /> : token ? <Content /> : <Error />;
|
|
236
|
-
};
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
## Performance Considerations
|
|
240
|
-
|
|
241
|
-
1. **Memoize request objects**: Always wrap the scopes object in `useMemo` to prevent unnecessary re-renders
|
|
242
|
-
```tsx
|
|
243
|
-
// Bad - creates new object on every render
|
|
244
|
-
const { token } = useAccessToken({ scopes: ['User.Read'] });
|
|
245
|
-
|
|
246
|
-
// Good - stable reference
|
|
247
|
-
const { token } = useAccessToken(
|
|
248
|
-
useMemo(() => ({ scopes: ['User.Read'] }), [])
|
|
249
|
-
);
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
2. **Dependent scopes**: Include scopes in the dependency array
|
|
253
|
-
```tsx
|
|
254
|
-
const { token } = useAccessToken(
|
|
255
|
-
useMemo(() => ({ scopes }), [scopes])
|
|
256
|
-
);
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
## MSAL v4 Under the Hood
|
|
260
|
-
|
|
261
|
-
While the hooks use a simple API, they internally use MSAL v4:
|
|
262
|
-
|
|
263
|
-
- Hooks accept `{ scopes: string[] }` for simplicity
|
|
264
|
-
- Internally converts to `{ request: { scopes: string[] } }` format
|
|
265
|
-
- The provider supports both legacy and modern formats
|
|
266
|
-
- All benefits of MSAL v4 are available (better security, performance, etc.)
|
|
267
|
-
|
|
268
|
-
## Troubleshooting
|
|
269
|
-
|
|
270
|
-
### Hook Returns No Account
|
|
271
|
-
|
|
272
|
-
- Ensure user is logged in
|
|
273
|
-
- Check that MSAL module is properly configured
|
|
274
|
-
- Verify `useCurrentAccount()` hook is wrapped in a component within the framework provider
|
|
275
|
-
|
|
276
|
-
### Token Acquisition Fails
|
|
277
|
-
|
|
278
|
-
- Verify scopes are properly configured in Azure AD
|
|
279
|
-
- Check user has necessary permissions for requested scopes
|
|
280
|
-
- Review browser console for MSAL errors
|
|
281
|
-
|
|
282
|
-
### Performance Issues
|
|
283
|
-
|
|
284
|
-
- Ensure scopes object is memoized with `useMemo`
|
|
285
|
-
- Check for unnecessary re-renders of parent components
|
|
286
|
-
|
|
287
|
-
## Related Documentation
|
|
288
|
-
|
|
289
|
-
- [MSAL Module Documentation](https://equinor.github.io/fusion-framework/modules/auth/msal/) - Core MSAL provider
|
|
290
|
-
- [Microsoft MSAL Browser Docs](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-browser) - Official MSAL documentation
|
|
291
|
-
- [Azure AD App Registration Guide](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) - App registration guide
|
package/docs/navigation.md
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
# Navigation
|
|
2
|
-
|
|
3
|
-
Set up client-side routing in your Fusion app using the framework-managed navigation module.
|
|
4
|
-
|
|
5
|
-
**Import:**
|
|
6
|
-
|
|
7
|
-
```ts
|
|
8
|
-
import { useRouter, useNavigationModule } from '@equinor/fusion-framework-react-app/navigation';
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Overview
|
|
12
|
-
|
|
13
|
-
The navigation module wraps React Router with Fusion's base path handling, ensuring your app's routes work correctly under the portal's URL structure (e.g. `/apps/my-app/...`). Use `useRouter` to create a router instance and pass it to `<RouterProvider>`.
|
|
14
|
-
|
|
15
|
-
> [!IMPORTANT]
|
|
16
|
-
> Do not use `createBrowserRouter` from React Router directly — it bypasses the Fusion base path and will cause routing conflicts inside the portal.
|
|
17
|
-
|
|
18
|
-
## Enable Navigation
|
|
19
|
-
|
|
20
|
-
The navigation module must be enabled in your app's configurator. Always pass the basename so routes resolve correctly under the portal's URL prefix:
|
|
21
|
-
|
|
22
|
-
```ts
|
|
23
|
-
import { enableNavigation } from '@equinor/fusion-framework-module-navigation';
|
|
24
|
-
|
|
25
|
-
export const configure = (configurator) => {
|
|
26
|
-
// Pass the basename that matches where the portal mounts your app.
|
|
27
|
-
// In a Fusion portal this is typically the app's appKey path, e.g. '/apps/my-app'.
|
|
28
|
-
enableNavigation(configurator, '/apps/my-app');
|
|
29
|
-
};
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Without a basename, routes will not be prefixed and will conflict with the portal's URL structure.
|
|
33
|
-
|
|
34
|
-
## useRouter
|
|
35
|
-
|
|
36
|
-
Creates a router instance from route definitions. The returned router is compatible with `<Router>` from `@equinor/fusion-framework-react-router`.
|
|
37
|
-
|
|
38
|
-
> [!WARNING]
|
|
39
|
-
> `useRouter` calls `INavigationProvider.createRouter()`, which is **deprecated** in the navigation module and emits a telemetry warning at runtime. For new apps, prefer `<Router>` from `@equinor/fusion-framework-react-router` directly — it handles the basename and Fusion context automatically without requiring `useRouter`.
|
|
40
|
-
|
|
41
|
-
> [!CAUTION]
|
|
42
|
-
> **Routes must be stable or memoised.** If you pass a new array reference on every render, the router will be recreated each time, resetting navigation state. Define routes outside the component or use `useMemo`.
|
|
43
|
-
|
|
44
|
-
### Minimal Routing Example
|
|
45
|
-
|
|
46
|
-
```tsx
|
|
47
|
-
import { useRouter } from '@equinor/fusion-framework-react-app/navigation';
|
|
48
|
-
import { Router } from '@equinor/fusion-framework-react-router';
|
|
49
|
-
|
|
50
|
-
const Home = () => <h1>Home</h1>;
|
|
51
|
-
const Settings = () => <h1>Settings</h1>;
|
|
52
|
-
|
|
53
|
-
// Define routes outside the component to keep them stable
|
|
54
|
-
const routes = [
|
|
55
|
-
{ path: '/', element: <Home /> },
|
|
56
|
-
{ path: '/settings', element: <Settings /> },
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
const App = () => {
|
|
60
|
-
const router = useRouter(routes);
|
|
61
|
-
return <Router router={router} />;
|
|
62
|
-
};
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
## useNavigationModule
|
|
66
|
-
|
|
67
|
-
Returns the navigation module provider instance directly. Use this for advanced scenarios where you need access to the full provider API (e.g. programmatic navigation outside of React Router's hooks).
|
|
68
|
-
|
|
69
|
-
**Signature:**
|
|
70
|
-
|
|
71
|
-
```ts
|
|
72
|
-
function useNavigationModule(): INavigationProvider;
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
**Throws** if the navigation module has not been enabled for the application.
|
|
76
|
-
|
|
77
|
-
## Prerequisites
|
|
78
|
-
|
|
79
|
-
- The navigation module must be enabled in your app's configurator via `enableNavigation`
|
|
80
|
-
- Do not mix `useRouter` with direct `createBrowserRouter` calls — use one approach consistently
|
package/docs/routing.md
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# Routing
|
|
2
|
-
|
|
3
|
-
The `@equinor/fusion-framework-react-app/routing` entry point re-exports the full public API of
|
|
4
|
-
`@equinor/fusion-framework-react-router` — including the `<Router>` component, the route builder
|
|
5
|
-
DSL, all React Router hooks, and types — so you can import everything from a single package without
|
|
6
|
-
adding `@equinor/fusion-framework-react-router` as a direct dependency.
|
|
7
|
-
|
|
8
|
-
## Installation
|
|
9
|
-
|
|
10
|
-
`@equinor/fusion-framework-react-router` is an **optional peer dependency**. Install it alongside the app package:
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
pnpm add @equinor/fusion-framework-react-router
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
## Usage
|
|
17
|
-
|
|
18
|
-
```ts
|
|
19
|
-
import { Router } from '@equinor/fusion-framework-react-app/routing';
|
|
20
|
-
import { layout, index, route, prefix } from '@equinor/fusion-framework-react-app/routing';
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Everything exported from `@equinor/fusion-framework-react-router` and its `/routes` DSL is
|
|
24
|
-
available from this single entry point.
|
|
25
|
-
|
|
26
|
-
## Enable navigation in your configurator
|
|
27
|
-
|
|
28
|
-
The `<Router>` component reads `history` and `basename` from the Fusion navigation module.
|
|
29
|
-
Enable it in your configurator before mounting:
|
|
30
|
-
|
|
31
|
-
```ts
|
|
32
|
-
import { enableNavigation } from '@equinor/fusion-framework-module-navigation';
|
|
33
|
-
import type { IAppConfigurator, AppEnv, Fusion } from '@equinor/fusion-framework-react-app';
|
|
34
|
-
|
|
35
|
-
export const configure = (
|
|
36
|
-
configurator: IAppConfigurator,
|
|
37
|
-
args: { fusion: Fusion; env: AppEnv },
|
|
38
|
-
) => {
|
|
39
|
-
enableNavigation(configurator, {
|
|
40
|
-
configure: (config) => {
|
|
41
|
-
config.setBasename(args.env.basename);
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
};
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## Define routes
|
|
48
|
-
|
|
49
|
-
```ts
|
|
50
|
-
// src/routes.ts
|
|
51
|
-
import { layout, index, route, prefix } from '@equinor/fusion-framework-react-app/routing';
|
|
52
|
-
|
|
53
|
-
export default layout('./Layout.tsx', [
|
|
54
|
-
index('./pages/HomePage.tsx'),
|
|
55
|
-
prefix('products', [
|
|
56
|
-
index('./pages/ProductsPage.tsx'),
|
|
57
|
-
route(':id', './pages/ProductPage.tsx'),
|
|
58
|
-
]),
|
|
59
|
-
]);
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## Mount the Router
|
|
63
|
-
|
|
64
|
-
```tsx
|
|
65
|
-
// src/Router.tsx
|
|
66
|
-
import { Router } from '@equinor/fusion-framework-react-app/routing';
|
|
67
|
-
import routes from './routes';
|
|
68
|
-
|
|
69
|
-
export default function AppRouter() {
|
|
70
|
-
return <Router routes={routes} />;
|
|
71
|
-
}
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Use routing hooks
|
|
75
|
-
|
|
76
|
-
All React Router hooks are re-exported from the same entry point:
|
|
77
|
-
|
|
78
|
-
```tsx
|
|
79
|
-
import { useNavigate, useParams, useLocation, Link } from '@equinor/fusion-framework-react-app/routing';
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
## See also
|
|
83
|
-
|
|
84
|
-
- [Getting started](/modules/react/router/getting-started) — full setup walkthrough for the standalone router package
|
|
85
|
-
- [Interop entry point](/modules/react/router/interop) — `MemoryRouter` and other react-router bridges for testing and mid-migration
|
|
86
|
-
- [Migration guide](/modules/react/router/migration) — moving from a plain react-router setup
|
package/docs/settings.md
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
# Settings
|
|
2
|
-
|
|
3
|
-
Persist and read app-specific user settings using the Fusion settings service.
|
|
4
|
-
|
|
5
|
-
**Import:**
|
|
6
|
-
|
|
7
|
-
```ts
|
|
8
|
-
import { useAppSetting, useAppSettings } from '@equinor/fusion-framework-react-app/settings';
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Overview
|
|
12
|
-
|
|
13
|
-
App settings let users customise their experience (theme, layout, filters) with values persisted per-user by the Fusion platform. The `useAppSetting` hook manages a single setting, while `useAppSettings` provides access to all settings at once.
|
|
14
|
-
|
|
15
|
-
## Type Your Settings
|
|
16
|
-
|
|
17
|
-
Use module augmentation to get type-safe setting keys and values:
|
|
18
|
-
|
|
19
|
-
```ts
|
|
20
|
-
declare module '@equinor/fusion-framework-react-app/settings' {
|
|
21
|
-
interface AppSettings {
|
|
22
|
-
theme: 'default' | 'light' | 'dark';
|
|
23
|
-
mode: 'simple' | 'advanced';
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## useAppSetting
|
|
29
|
-
|
|
30
|
-
Manages a single setting by key. Returns the current value and a setter function, similar to `useState`.
|
|
31
|
-
|
|
32
|
-
**Signature:**
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
function useAppSetting<TSettings, TProp extends keyof TSettings>(
|
|
36
|
-
prop: TProp,
|
|
37
|
-
defaultValue?: TSettings[TProp],
|
|
38
|
-
hooks?: AppSettingsStatusHooks & {
|
|
39
|
-
onError?: (error: Error | null) => void;
|
|
40
|
-
onUpdated?: () => void;
|
|
41
|
-
},
|
|
42
|
-
): [TSettings[TProp] | undefined, (update: TSettings[TProp] | ((current: TSettings[TProp] | undefined) => TSettings[TProp])) => void];
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### Example
|
|
46
|
-
|
|
47
|
-
```tsx
|
|
48
|
-
import { useCallback } from 'react';
|
|
49
|
-
import { useAppSetting } from '@equinor/fusion-framework-react-app/settings';
|
|
50
|
-
|
|
51
|
-
const ThemeSwitcher = () => {
|
|
52
|
-
const [theme, setTheme] = useAppSetting('theme', 'default');
|
|
53
|
-
const [mode, setMode] = useAppSetting('mode', 'simple');
|
|
54
|
-
|
|
55
|
-
const toggleMode = useCallback(() => {
|
|
56
|
-
setMode((current) => (current === 'simple' ? 'advanced' : 'simple'));
|
|
57
|
-
}, [setMode]);
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<div>
|
|
61
|
-
{/* cast: e.target.value is string, but setTheme expects the union type */}
|
|
62
|
-
<select value={theme ?? 'default'} onChange={(e) => setTheme(e.target.value as AppSettings['theme'])}>
|
|
63
|
-
<option value="default">Default</option>
|
|
64
|
-
<option value="light">Light</option>
|
|
65
|
-
<option value="dark">Dark</option>
|
|
66
|
-
</select>
|
|
67
|
-
<button onClick={toggleMode}>Toggle mode ({mode})</button>
|
|
68
|
-
</div>
|
|
69
|
-
);
|
|
70
|
-
};
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## useAppSettings
|
|
74
|
-
|
|
75
|
-
Returns all settings as a single object with a bulk setter. Use `useAppSetting` for individual settings when possible — `useAppSettings` triggers a re-render on _any_ setting change.
|
|
76
|
-
|
|
77
|
-
**Signature:**
|
|
78
|
-
|
|
79
|
-
```ts
|
|
80
|
-
function useAppSettings<TSettings>(
|
|
81
|
-
defaultValue?: TSettings,
|
|
82
|
-
hooks?: AppSettingsStatusHooks & {
|
|
83
|
-
onError?: (error: Error | null) => void;
|
|
84
|
-
onUpdated?: () => void;
|
|
85
|
-
},
|
|
86
|
-
): [TSettings, (update: TSettings | ((current: TSettings | undefined) => TSettings)) => void];
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
> [!WARNING]
|
|
90
|
-
> **`setSettings` must include all settings, not just the ones you want to change.** Use a callback to merge with the current state:
|
|
91
|
-
>
|
|
92
|
-
> ```ts
|
|
93
|
-
> setSettings((current) => ({ ...current, theme: 'dark' }));
|
|
94
|
-
> ```
|
|
95
|
-
|
|
96
|
-
### Example
|
|
97
|
-
|
|
98
|
-
```tsx
|
|
99
|
-
import { useCallback } from 'react';
|
|
100
|
-
import { useAppSettings } from '@equinor/fusion-framework-react-app/settings';
|
|
101
|
-
|
|
102
|
-
const SettingsPanel = () => {
|
|
103
|
-
const [settings, setSettings] = useAppSettings();
|
|
104
|
-
|
|
105
|
-
const updateTheme = useCallback(
|
|
106
|
-
(theme: AppSettings['theme']) => setSettings((current) => ({ ...current, theme })),
|
|
107
|
-
[setSettings],
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
return <ThemeSelector value={settings?.theme} onChange={updateTheme} />;
|
|
111
|
-
};
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
## Status Hooks
|
|
115
|
-
|
|
116
|
-
Both hooks accept optional status callbacks for loading, updating, and error states:
|
|
117
|
-
|
|
118
|
-
```tsx
|
|
119
|
-
const [loading, setLoading] = useState(false);
|
|
120
|
-
const [updating, setUpdating] = useState(false);
|
|
121
|
-
const [error, setError] = useState<Error | null>(null);
|
|
122
|
-
|
|
123
|
-
const [theme, setTheme] = useAppSetting('theme', 'default', {
|
|
124
|
-
onLoading: setLoading,
|
|
125
|
-
onUpdating: setUpdating,
|
|
126
|
-
onError: setError,
|
|
127
|
-
onUpdated: useCallback(() => console.log('Saved'), []),
|
|
128
|
-
});
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
> [!NOTE]
|
|
132
|
-
> `onUpdating` and `onLoading` reflect the _global_ settings state, not individual settings. Disable update buttons while either is `true`.
|
|
133
|
-
|
|
134
|
-
## Notes
|
|
135
|
-
|
|
136
|
-
- Settings are async — there is a loading phase when the component mounts and an updating phase when values are saved
|
|
137
|
-
- `useAppSetting` captures the `hooks` object once on mount (via `useState`); **the callbacks must be stable before the first render** — changes after mount are ignored
|
|
138
|
-
- `useAppSettings` reads `hooks` on every render; **memoise callbacks** (e.g. with `useCallback`) to avoid unnecessary re-renders
|
|
139
|
-
- Settings are persisted per-user per-app by the Fusion platform
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { mockFramework } from '@equinor/fusion-framework/mock';
|
|
4
|
-
import { enableAppManifestMock } from '@equinor/fusion-framework-app/mock';
|
|
5
|
-
import type { AppManifest, AppModule } from '@equinor/fusion-framework-module-app';
|
|
6
|
-
|
|
7
|
-
import { Apploader } from '../apploader/Apploader';
|
|
8
|
-
import { renderAppComponent } from '@equinor/fusion-framework-vitest-plugin-react-app/test';
|
|
9
|
-
|
|
10
|
-
// resolved from this test file rather than hardcoded, so it survives a package move
|
|
11
|
-
const fixturesUri = new URL('./fixtures', import.meta.url).pathname;
|
|
12
|
-
|
|
13
|
-
describe('Apploader', () => {
|
|
14
|
-
it('mounts the child app’s rendered content once its script loads', async () => {
|
|
15
|
-
const manifest: AppManifest = {
|
|
16
|
-
appKey: 'child-app',
|
|
17
|
-
displayName: 'Child App',
|
|
18
|
-
description: 'A child application',
|
|
19
|
-
type: 'standalone',
|
|
20
|
-
build: { version: '1.0.0', entryPoint: 'apploader-child-script.ts', assetPath: '' },
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const fusion = await mockFramework<[AppModule]>((configurator) =>
|
|
24
|
-
enableAppManifestMock(configurator, { manifest }, fixturesUri),
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
const { container } = await renderAppComponent(<Apploader appKey="child-app" />, { fusion });
|
|
28
|
-
|
|
29
|
-
// the loading state renders synchronously, before the script's dynamic import resolves
|
|
30
|
-
expect(container.textContent).toContain('Loading child-app');
|
|
31
|
-
await vi.waitFor(() => expect(container.textContent).toContain('mounted: child-app'));
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('surfaces the load error instead of throwing when the app’s script fails to import', async () => {
|
|
35
|
-
const manifest: AppManifest = {
|
|
36
|
-
appKey: 'broken-app',
|
|
37
|
-
displayName: 'Broken App',
|
|
38
|
-
description: 'An application whose build entry point does not exist',
|
|
39
|
-
type: 'standalone',
|
|
40
|
-
build: { version: '1.0.0', entryPoint: 'does-not-exist.ts', assetPath: '' },
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const fusion = await mockFramework<[AppModule]>((configurator) =>
|
|
44
|
-
enableAppManifestMock(configurator, { manifest }, fixturesUri),
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
const { container } = await renderAppComponent(<Apploader appKey="broken-app" />, { fusion });
|
|
48
|
-
|
|
49
|
-
await vi.waitFor(() => expect(container.textContent).toContain('Error loading broken-app'));
|
|
50
|
-
});
|
|
51
|
-
});
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { AppScriptModule } from '@equinor/fusion-framework-module-app';
|
|
2
|
-
|
|
3
|
-
// a minimal stand-in for a real app bundle's entry point, dynamically imported by `App.initialize()`
|
|
4
|
-
export const renderApp: AppScriptModule['renderApp'] = (el, { env }) => {
|
|
5
|
-
el.textContent = `mounted: ${env.manifest?.appKey}`;
|
|
6
|
-
return () => {
|
|
7
|
-
el.textContent = '';
|
|
8
|
-
};
|
|
9
|
-
};
|