@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.
Files changed (93) hide show
  1. package/dist/esm/version.js +1 -1
  2. package/dist/tsconfig.tsbuildinfo +1 -1
  3. package/dist/types/version.d.ts +1 -1
  4. package/package.json +33 -30
  5. package/CHANGELOG.md +0 -2785
  6. package/docs/ag-grid.md +0 -70
  7. package/docs/analytics.md +0 -122
  8. package/docs/app.md +0 -148
  9. package/docs/apploader.md +0 -110
  10. package/docs/bookmark.md +0 -108
  11. package/docs/context.md +0 -88
  12. package/docs/feature-flag.md +0 -119
  13. package/docs/framework.md +0 -93
  14. package/docs/help-center.md +0 -88
  15. package/docs/http.md +0 -118
  16. package/docs/msal.md +0 -291
  17. package/docs/navigation.md +0 -80
  18. package/docs/routing.md +0 -86
  19. package/docs/settings.md +0 -139
  20. package/src/__tests__/Apploader.test.tsx +0 -51
  21. package/src/__tests__/fixtures/apploader-child-script.ts +0 -9
  22. package/src/__tests__/render-app.test.tsx +0 -113
  23. package/src/__tests__/testApp.test.tsx +0 -76
  24. package/src/__tests__/useAccessToken.test.tsx +0 -51
  25. package/src/__tests__/useAppSetting.test.tsx +0 -133
  26. package/src/__tests__/useAppSettings.test.tsx +0 -147
  27. package/src/__tests__/useCurrentAccount.test.tsx +0 -32
  28. package/src/__tests__/useCurrentBookmark.test.tsx +0 -108
  29. package/src/__tests__/useCurrentContext.test.tsx +0 -72
  30. package/src/__tests__/useFeature.test.tsx +0 -104
  31. package/src/__tests__/useHelpCenter.test.tsx +0 -64
  32. package/src/__tests__/useStateSyncEvents.test.ts +0 -58
  33. package/src/__tests__/useToken.test.tsx +0 -71
  34. package/src/__tests__/useTrackFeature.test.tsx +0 -83
  35. package/src/ag-grid/community.ts +0 -11
  36. package/src/ag-grid/enterprise.ts +0 -11
  37. package/src/ag-grid/react.ts +0 -11
  38. package/src/ag-grid/testing.ts +0 -19
  39. package/src/ag-grid/theme.ts +0 -17
  40. package/src/ag-grid/useTheme.ts +0 -21
  41. package/src/analytics/README.md +0 -148
  42. package/src/analytics/index.ts +0 -9
  43. package/src/analytics/useTrackFeature.ts +0 -73
  44. package/src/apploader/Apploader.tsx +0 -50
  45. package/src/apploader/README.md +0 -81
  46. package/src/apploader/index.ts +0 -10
  47. package/src/apploader/useApploader.ts +0 -107
  48. package/src/bookmark/index.ts +0 -20
  49. package/src/bookmark/useBookmark.ts +0 -25
  50. package/src/bookmark/useCurrentBookmark.ts +0 -54
  51. package/src/context/index.ts +0 -14
  52. package/src/context/useContextProvider.ts +0 -12
  53. package/src/context/useCurrentContext.ts +0 -20
  54. package/src/create-component.tsx +0 -120
  55. package/src/create-legacy-app.tsx +0 -51
  56. package/src/feature-flag/README.md +0 -20
  57. package/src/feature-flag/enable-feature-flag.ts +0 -79
  58. package/src/feature-flag/index.ts +0 -17
  59. package/src/feature-flag/useFeature.ts +0 -90
  60. package/src/framework/index.ts +0 -14
  61. package/src/framework/useFrameworkCurrentContext.ts +0 -15
  62. package/src/help-center/README.md +0 -21
  63. package/src/help-center/event-name.ts +0 -1
  64. package/src/help-center/index.ts +0 -11
  65. package/src/help-center/useHelpCenter.ts +0 -162
  66. package/src/http/index.ts +0 -10
  67. package/src/http/selectors.ts +0 -9
  68. package/src/index.ts +0 -46
  69. package/src/make-component.tsx +0 -102
  70. package/src/msal/index.ts +0 -16
  71. package/src/msal/useAccessToken.ts +0 -26
  72. package/src/msal/useCurrentAccount.ts +0 -21
  73. package/src/msal/useToken.ts +0 -63
  74. package/src/navigation/index.ts +0 -10
  75. package/src/navigation/useNavigationModule.ts +0 -10
  76. package/src/navigation/useRouter.ts +0 -35
  77. package/src/render-app.ts +0 -35
  78. package/src/render-component.tsx +0 -55
  79. package/src/routing/index.ts +0 -33
  80. package/src/settings/README.md +0 -123
  81. package/src/settings/index.ts +0 -12
  82. package/src/settings/useAppSetting.ts +0 -113
  83. package/src/settings/useAppSettings.ts +0 -103
  84. package/src/settings/useAppSettingsStatus.ts +0 -52
  85. package/src/state/index.ts +0 -12
  86. package/src/state/useAppState.ts +0 -299
  87. package/src/state/useStateSyncEvents.ts +0 -59
  88. package/src/useAppEnvironmentVariables.ts +0 -59
  89. package/src/useAppModule.ts +0 -45
  90. package/src/useAppModules.ts +0 -21
  91. package/src/version.ts +0 -2
  92. package/tsconfig.json +0 -54
  93. 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
@@ -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
- };