@equinor/fusion-framework-module-context 8.0.3 → 9.0.0-next.0
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/CHANGELOG.md +76 -0
- package/README.md +78 -2
- package/dist/esm/ContextModuleConfig.js +2 -0
- package/dist/esm/ContextModuleConfig.js.map +1 -0
- package/dist/esm/ContextModuleConfigurator.interface.js +2 -0
- package/dist/esm/ContextModuleConfigurator.interface.js.map +1 -0
- package/dist/esm/ContextModuleConfigurator.js +211 -0
- package/dist/esm/ContextModuleConfigurator.js.map +1 -0
- package/dist/esm/ContextProvider.js +1 -1
- package/dist/esm/ContextProvider.js.map +1 -1
- package/dist/esm/__tests__/ContextModuleConfigurator.test.js +282 -0
- package/dist/esm/__tests__/ContextModuleConfigurator.test.js.map +1 -0
- package/dist/esm/__tests__/mock/context-mock.test.js +101 -0
- package/dist/esm/__tests__/mock/context-mock.test.js.map +1 -0
- package/dist/esm/__tests__/mock/create-context-item-factory.test.js +45 -0
- package/dist/esm/__tests__/mock/create-context-item-factory.test.js.map +1 -0
- package/dist/esm/__tests__/mock/create-context-items.test.js +46 -0
- package/dist/esm/__tests__/mock/create-context-items.test.js.map +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/mock/ContextMockConfigurator.js +206 -0
- package/dist/esm/mock/ContextMockConfigurator.js.map +1 -0
- package/dist/esm/mock/fixtures/create-context-item-factory.js +50 -0
- package/dist/esm/mock/fixtures/create-context-item-factory.js.map +1 -0
- package/dist/esm/mock/fixtures/create-context-items.js +53 -0
- package/dist/esm/mock/fixtures/create-context-items.js.map +1 -0
- package/dist/esm/mock/fixtures/index.js +22 -0
- package/dist/esm/mock/fixtures/index.js.map +1 -0
- package/dist/esm/mock/fixtures/string-to-seed.js +15 -0
- package/dist/esm/mock/fixtures/string-to-seed.js.map +1 -0
- package/dist/esm/mock/index.js +31 -0
- package/dist/esm/mock/index.js.map +1 -0
- package/dist/esm/mock/module.js +41 -0
- package/dist/esm/mock/module.js.map +1 -0
- package/dist/esm/module.js +35 -6
- package/dist/esm/module.js.map +1 -1
- package/dist/esm/{get-context-selector.js → selectors/get-context-selector.js} +1 -4
- package/dist/esm/selectors/get-context-selector.js.map +1 -0
- package/dist/esm/selectors/index.js +13 -0
- package/dist/esm/selectors/index.js.map +1 -0
- package/dist/esm/{query-context-selector.js → selectors/query-context-selector.js} +1 -1
- package/dist/esm/selectors/query-context-selector.js.map +1 -0
- package/dist/esm/{related-context-selector.js → selectors/related-context-selector.js} +1 -1
- package/dist/esm/selectors/related-context-selector.js.map +1 -0
- package/dist/esm/utils/enable-context.js +1 -1
- package/dist/esm/utils/enable-context.js.map +1 -1
- package/dist/esm/utils/index.js +2 -0
- package/dist/esm/utils/index.js.map +1 -1
- package/dist/esm/utils/parse-context-item.js.map +1 -0
- package/dist/esm/utils/resolve-initial-context.js +3 -1
- package/dist/esm/utils/resolve-initial-context.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/{configurator.d.ts → ContextModuleConfig.d.ts} +4 -64
- package/dist/types/ContextModuleConfigurator.d.ts +97 -0
- package/dist/types/{ContextConfigBuilder.d.ts → ContextModuleConfigurator.interface.d.ts} +27 -47
- package/dist/types/ContextProvider.d.ts +1 -1
- package/dist/types/__tests__/ContextModuleConfigurator.test.d.ts +1 -0
- package/dist/types/__tests__/mock/context-mock.test.d.ts +1 -0
- package/dist/types/__tests__/mock/create-context-item-factory.test.d.ts +1 -0
- package/dist/types/__tests__/mock/create-context-items.test.d.ts +1 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/mock/ContextMockConfigurator.d.ts +155 -0
- package/dist/types/mock/fixtures/create-context-item-factory.d.ts +40 -0
- package/dist/types/mock/fixtures/create-context-items.d.ts +59 -0
- package/dist/types/mock/fixtures/index.d.ts +21 -0
- package/dist/types/mock/fixtures/string-to-seed.d.ts +12 -0
- package/dist/types/mock/index.d.ts +30 -0
- package/dist/types/mock/module.d.ts +38 -0
- package/dist/types/module.d.ts +5 -3
- package/dist/types/{get-context-selector.d.ts → selectors/get-context-selector.d.ts} +1 -3
- package/dist/types/selectors/index.d.ts +12 -0
- package/dist/types/{query-context-selector.d.ts → selectors/query-context-selector.d.ts} +1 -1
- package/dist/types/{related-context-selector.d.ts → selectors/related-context-selector.d.ts} +1 -1
- package/dist/types/utils/enable-context.d.ts +4 -5
- package/dist/types/utils/index.d.ts +2 -0
- package/dist/types/{parse-context-item.d.ts → utils/parse-context-item.d.ts} +1 -1
- package/dist/types/utils/resolve-initial-context.d.ts +1 -1
- package/dist/types/version.d.ts +1 -1
- package/docs/data-model.md +134 -0
- package/docs/lifecycle.md +163 -0
- package/docs/recipes.md +89 -0
- package/package.json +36 -8
- package/src/ContextModuleConfig.ts +147 -0
- package/src/ContextModuleConfigurator.interface.ts +145 -0
- package/src/ContextModuleConfigurator.ts +295 -0
- package/src/ContextProvider.ts +1 -2
- package/src/__tests__/ContextModuleConfigurator.test.ts +412 -0
- package/src/__tests__/mock/context-mock.test.ts +137 -0
- package/src/__tests__/mock/create-context-item-factory.test.ts +60 -0
- package/src/__tests__/mock/create-context-items.test.ts +58 -0
- package/src/index.ts +3 -5
- package/src/mock/ContextMockConfigurator.ts +244 -0
- package/src/mock/fixtures/create-context-item-factory.ts +62 -0
- package/src/mock/fixtures/create-context-items.ts +80 -0
- package/src/mock/fixtures/index.ts +28 -0
- package/src/mock/fixtures/string-to-seed.ts +18 -0
- package/src/mock/index.ts +33 -0
- package/src/mock/module.ts +54 -0
- package/src/module.ts +45 -17
- package/src/{get-context-selector.ts → selectors/get-context-selector.ts} +2 -6
- package/src/selectors/index.ts +12 -0
- package/src/{query-context-selector.ts → selectors/query-context-selector.ts} +2 -2
- package/src/{related-context-selector.ts → selectors/related-context-selector.ts} +2 -2
- package/src/utils/enable-context.ts +6 -10
- package/src/utils/index.ts +2 -0
- package/src/{parse-context-item.ts → utils/parse-context-item.ts} +1 -1
- package/src/utils/resolve-initial-context.ts +5 -2
- package/src/version.ts +1 -1
- package/tsconfig.json +6 -0
- package/vitest.config.ts +11 -0
- package/dist/esm/ContextConfigBuilder.js +0 -175
- package/dist/esm/ContextConfigBuilder.js.map +0 -1
- package/dist/esm/configurator.js +0 -104
- package/dist/esm/configurator.js.map +0 -1
- package/dist/esm/get-context-selector.js.map +0 -1
- package/dist/esm/parse-context-item.js.map +0 -1
- package/dist/esm/query-context-selector.js.map +0 -1
- package/dist/esm/related-context-selector.js.map +0 -1
- package/src/ContextConfigBuilder.ts +0 -258
- package/src/configurator.ts +0 -300
- /package/dist/esm/{parse-context-item.js → utils/parse-context-item.js} +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { IModulesConfigurator } from '@equinor/fusion-framework-module';
|
|
2
|
+
import { type ContextModule } from '../module';
|
|
3
|
+
import { ContextMockConfigurator } from './ContextMockConfigurator';
|
|
4
|
+
/**
|
|
5
|
+
* The context module with an in-memory mock configurator instead of a real
|
|
6
|
+
* context API.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* Only `configure` differs from the real module. `initialize` is the production
|
|
10
|
+
* one, untouched, so a test exercises the real `ContextProvider` startup path,
|
|
11
|
+
* `validateContext`/`resolveContext`, and parent-context propagation — a
|
|
12
|
+
* rehearsal of the module, not a stand-in for it.
|
|
13
|
+
*/
|
|
14
|
+
export declare const contextMockModule: ContextModule;
|
|
15
|
+
/**
|
|
16
|
+
* Configuration callback for {@link enableContextMock}.
|
|
17
|
+
*/
|
|
18
|
+
export type ContextMockConfigFn = (mock: ContextMockConfigurator) => void | Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Enables the context module against in-memory seeded data, so a test needs no
|
|
21
|
+
* context API, no HTTP mock, and no service-discovery mock.
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* Registered last, this replaces whichever context module the configurator
|
|
25
|
+
* already carries, so it works on a `FrameworkConfigurator` that pre-registers
|
|
26
|
+
* the real one.
|
|
27
|
+
*
|
|
28
|
+
* @param configurator - The modules configurator to register on.
|
|
29
|
+
* @param configure - Optional callback to seed context items or override resolution.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* enableContextMock(configurator, (mock) => {
|
|
34
|
+
* mock.setCurrentContext({ id: 'my-ctx', type: { id: 'ProjectMaster' }, value: {} });
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare const enableContextMock: (configurator: IModulesConfigurator<any, any>, configure?: ContextMockConfigFn) => void;
|
package/dist/types/module.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ import type { Module } from '@equinor/fusion-framework-module';
|
|
|
2
2
|
import type { EventModule } from '@equinor/fusion-framework-module-event';
|
|
3
3
|
import type { ServicesModule } from '@equinor/fusion-framework-module-services';
|
|
4
4
|
import type { NavigationModule } from '@equinor/fusion-framework-module-navigation';
|
|
5
|
-
import { type
|
|
5
|
+
import { type TelemetryModule } from '@equinor/fusion-framework-module-telemetry';
|
|
6
|
+
import { type IContextModuleConfigurator } from './ContextModuleConfigurator';
|
|
6
7
|
import { type IContextProvider } from './ContextProvider';
|
|
7
8
|
/**
|
|
8
9
|
* Literal type identifying the context module within the Fusion Framework module system.
|
|
@@ -23,14 +24,15 @@ export declare const moduleKey: ContextModuleKey;
|
|
|
23
24
|
* @typeParam ContextModuleKey - The unique key identifying the context module.
|
|
24
25
|
* @typeParam IContextProvider - The provider interface for context-related services.
|
|
25
26
|
* @typeParam IContextModuleConfigurator - The configurator interface for customizing the context module.
|
|
26
|
-
* @typeParam [ServicesModule, EventModule, NavigationModule] - The tuple of dependent modules required by the context module.
|
|
27
|
+
* @typeParam [ServicesModule, EventModule, NavigationModule, TelemetryModule] - The tuple of dependent modules required by the context module.
|
|
27
28
|
*
|
|
28
29
|
* @see Module
|
|
29
30
|
*/
|
|
30
31
|
export type ContextModule = Module<ContextModuleKey, IContextProvider, IContextModuleConfigurator, [
|
|
31
32
|
ServicesModule,
|
|
32
33
|
EventModule,
|
|
33
|
-
NavigationModule
|
|
34
|
+
NavigationModule,
|
|
35
|
+
TelemetryModule
|
|
34
36
|
]>;
|
|
35
37
|
/**
|
|
36
38
|
* The `module` object implements the `ContextModule` interface and provides the configuration,
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import type { ContextItem } from '
|
|
1
|
+
import type { ContextItem } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
* Parse the response from the GetContext API into a context item.
|
|
4
4
|
* @param response The response object containing the context item.
|
|
5
5
|
* @returns A promise that resolves to the context item.
|
|
6
6
|
*/
|
|
7
7
|
export declare const getContextSelector: (response: Response) => Promise<ContextItem>;
|
|
8
|
-
export { queryContextSelector } from './query-context-selector';
|
|
9
|
-
export { relatedContextSelector } from './related-context-selector';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response selectors that parse raw context API responses into {@link ContextItem}s.
|
|
3
|
+
*
|
|
4
|
+
* - {@link getContextSelector} — parses a single-item GetContext response.
|
|
5
|
+
* - {@link queryContextSelector} — parses a multi-item QueryContext response.
|
|
6
|
+
* - {@link relatedContextSelector} — parses a multi-item RelatedContext response.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
export { getContextSelector } from './get-context-selector';
|
|
11
|
+
export { queryContextSelector } from './query-context-selector';
|
|
12
|
+
export { relatedContextSelector } from './related-context-selector';
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import type { IModulesConfigurator
|
|
2
|
-
import type {
|
|
3
|
-
import type { ContextConfigBuilder } from '../ContextConfigBuilder';
|
|
1
|
+
import type { IModulesConfigurator } from '@equinor/fusion-framework-module';
|
|
2
|
+
import type { ContextConfigBuilderCallback } from '../ContextModuleConfigurator.interface';
|
|
4
3
|
/**
|
|
5
4
|
* Enables context configuration for a given modules configurator.
|
|
6
5
|
*
|
|
7
6
|
* @param configurator - The modules configurator instance to which the context module will be added.
|
|
8
|
-
* @param builder - An optional function that receives
|
|
7
|
+
* @param builder - An optional function that receives the {@link IContextModuleConfigurator}. This function can be used to further configure the context module. It can be asynchronous.
|
|
9
8
|
*
|
|
10
9
|
* @remarks
|
|
11
10
|
* This utility function adds the context module to the provided configurator and optionally applies additional configuration using the provided builder function.
|
|
@@ -21,4 +20,4 @@ import type { ContextConfigBuilder } from '../ContextConfigBuilder';
|
|
|
21
20
|
* };
|
|
22
21
|
* ```
|
|
23
22
|
*/
|
|
24
|
-
export declare const enableContext: (configurator: IModulesConfigurator<any, any>, builder?:
|
|
23
|
+
export declare const enableContext: (configurator: IModulesConfigurator<any, any>, builder?: ContextConfigBuilderCallback) => void;
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* - {@link resolveInitialContext} — default initial-context resolver (path → parent fallback).
|
|
6
6
|
* - {@link extractContextIdFromPath} — extract a GUID context ID from a URL path.
|
|
7
7
|
* - {@link resolveContextFromPath} — resolve a context item from a URL path.
|
|
8
|
+
* - {@link parseContextItem} — parse a raw API context entity into a `ContextItem`.
|
|
8
9
|
*
|
|
9
10
|
* @packageDocumentation
|
|
10
11
|
*/
|
|
11
12
|
export { enableContext } from './enable-context';
|
|
12
13
|
export { resolveInitialContext } from './resolve-initial-context';
|
|
13
14
|
export { extractContextIdFromPath, resolveContextFromPath } from './resolve-context-from-path';
|
|
15
|
+
export { parseContextItem } from './parse-context-item';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ApiVersion, ApiContextEntity } from '@equinor/fusion-framework-module-services/context';
|
|
2
|
-
import type { ContextItem } from '
|
|
2
|
+
import type { ContextItem } from '../types';
|
|
3
3
|
/**
|
|
4
4
|
* Parses an ApiContextEntity object into a ContextItem object.
|
|
5
5
|
* @param item The ApiContextEntity object to parse.
|
package/dist/types/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "
|
|
1
|
+
export declare const version = "9.0.0-next.0";
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Data Model
|
|
2
|
+
|
|
3
|
+
What a context item actually looks like, how context *types* relate to each other, and the
|
|
4
|
+
shapes used to query and filter them.
|
|
5
|
+
|
|
6
|
+
## `ContextItem`
|
|
7
|
+
|
|
8
|
+
A context item is the thing your application or portal is currently scoped to — a project, a
|
|
9
|
+
facility, a contract, or any other entity the Fusion context API knows about.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
type ContextItem<TValue = Record<string, unknown>> = {
|
|
13
|
+
id: string;
|
|
14
|
+
type: ContextItemType;
|
|
15
|
+
value: TValue;
|
|
16
|
+
externalId?: string;
|
|
17
|
+
source?: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
subTitle?: string;
|
|
20
|
+
isActive?: boolean;
|
|
21
|
+
isDeleted?: boolean;
|
|
22
|
+
created?: Date;
|
|
23
|
+
updated?: Date;
|
|
24
|
+
graphic?: string | { type: 'html' | 'svg'; content: string };
|
|
25
|
+
meta?: string | { type: 'html' | 'svg'; content: string };
|
|
26
|
+
};
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The fields that matter most day to day:
|
|
30
|
+
|
|
31
|
+
- **`id`** — the identifier the context module itself uses everywhere: `setCurrentContextById`,
|
|
32
|
+
`extractContextIdFromPath`, cache keys, and equality checks (`connectParentContext` only
|
|
33
|
+
reacts when `id` actually changes).
|
|
34
|
+
- **`type`** — see [`ContextItemType`](#contextitemtype-and-hierarchies) below; this is what
|
|
35
|
+
`contextType` allow-lists and `validateContext` check against.
|
|
36
|
+
- **`value`** — the domain payload for this item (e.g. the actual project or facility record).
|
|
37
|
+
Type it with the generic parameter, `ContextItem<MyProjectShape>`, when you know the shape.
|
|
38
|
+
- **`externalId`** and **`source`** — identifiers from an upstream system, when the context item
|
|
39
|
+
was imported or mirrored from somewhere other than the context API itself.
|
|
40
|
+
- **`title`** / **`subTitle`** / **`graphic`** / **`meta`** — presentation hints for UI
|
|
41
|
+
components (e.g. a context switcher) to render without needing to know the `value` shape.
|
|
42
|
+
|
|
43
|
+
> [!NOTE]
|
|
44
|
+
> `ContextItem` is currently a hand-written type, not yet backed by a runtime schema — see
|
|
45
|
+
> [issue #5122](https://github.com/equinor/fusion-framework/issues/5122). Values coming back
|
|
46
|
+
> from a real context API are not validated at the type boundary; a custom `setContextClient`
|
|
47
|
+
> is responsible for shaping its own responses correctly.
|
|
48
|
+
|
|
49
|
+
## `ContextItemType` and hierarchies
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
interface ContextItemType {
|
|
53
|
+
id: string;
|
|
54
|
+
isChildType?: boolean;
|
|
55
|
+
parentTypeIds?: string[];
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Every context item carries a `type` that says what *kind* of entity it is — `'ProjectMaster'`,
|
|
60
|
+
`'Facility'`, `'Contract'`, and so on. Types can be organized as a shallow hierarchy:
|
|
61
|
+
`isChildType: true` plus `parentTypeIds` marks a type as a child of one or more parent types
|
|
62
|
+
(e.g. a `'Facility'` that belongs to a `'ProjectMaster'`).
|
|
63
|
+
|
|
64
|
+
This hierarchy is informational — nothing in this module walks `parentTypeIds` automatically.
|
|
65
|
+
It exists so that:
|
|
66
|
+
|
|
67
|
+
- application code can build a "these types are related" UI (e.g. grouping facilities under
|
|
68
|
+
their parent project) without a second API call,
|
|
69
|
+
- a custom `resolveContext` or `contextParameterFn` can use `parentTypeIds` to broaden a search
|
|
70
|
+
or a relation lookup by type family instead of a single exact type ID.
|
|
71
|
+
|
|
72
|
+
`setContextType(['ProjectMaster'])` restricts which type **IDs** `validateContext` accepts — it
|
|
73
|
+
does not expand automatically to include child or parent types unless you list them explicitly
|
|
74
|
+
or write a custom `setValidateContext`.
|
|
75
|
+
|
|
76
|
+
> [!NOTE]
|
|
77
|
+
> Type ID matching in `validateContext`'s default implementation is case-insensitive, so
|
|
78
|
+
> `'projectmaster'` and `'ProjectMaster'` are treated the same.
|
|
79
|
+
|
|
80
|
+
## Querying and relating
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
type QueryContextParameters = {
|
|
84
|
+
search?: string;
|
|
85
|
+
filter?: { type?: string[]; externalId?: string };
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
type RelatedContextParameters = {
|
|
89
|
+
item: ContextItem;
|
|
90
|
+
filter?: { type?: string[] };
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
type ContextFilterFn = (items: ContextItem[]) => ContextItem[];
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- **`QueryContextParameters`** is what `queryContext`/`queryContextAsync` sends to the query
|
|
97
|
+
client, after `contextParameterFn` has had a chance to transform it — see
|
|
98
|
+
[`setContextParameterFn`](../README.md#configuration-reference) and the
|
|
99
|
+
[OData recipe](recipes.md#odata-query-parameters) for a non-default mapping.
|
|
100
|
+
- **`RelatedContextParameters`** is what `relatedContexts`/`relatedContextsAsync` sends when
|
|
101
|
+
looking up items related to a given `item` — this is what the default `resolveContext`
|
|
102
|
+
uses internally when validation fails; see [Lifecycle](lifecycle.md#resolving-context).
|
|
103
|
+
- **`ContextFilterFn`** (`setContextFilter`) runs *after* a query returns, entirely client-side —
|
|
104
|
+
use it to hide items the API returned but that shouldn't be selectable (e.g. `isDeleted`
|
|
105
|
+
items), as opposed to `contextParameterFn`, which shapes the request itself.
|
|
106
|
+
|
|
107
|
+
## The context client
|
|
108
|
+
|
|
109
|
+
`ContextClient` is the piece that actually holds "the current context" and fetches a single
|
|
110
|
+
item by ID. It underlies `currentContext`/`currentContext$` on `ContextProvider` — the provider
|
|
111
|
+
does not keep its own separate copy of the state.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
type GetContextParameters = { id: string };
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`currentContext$` (and `currentContext`) can be in exactly three states, and the difference
|
|
118
|
+
matters for anything that reacts to it:
|
|
119
|
+
|
|
120
|
+
| Value | Meaning |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `undefined` | No context has been resolved yet — the provider hasn't initialized (or finished initial-context resolution). |
|
|
123
|
+
| `null` | Context was explicitly cleared, e.g. via `clearCurrentContext()`. |
|
|
124
|
+
| a `ContextItem` | A context is actively set. |
|
|
125
|
+
|
|
126
|
+
Treat `undefined` and `null` differently in UI code: `undefined` usually means "still loading,
|
|
127
|
+
don't show an empty state yet", while `null` means "there really is no context — show the
|
|
128
|
+
picker".
|
|
129
|
+
|
|
130
|
+
## Errors
|
|
131
|
+
|
|
132
|
+
`ContextClient`/the query clients throw `FusionContextSearchError` for domain-specific search
|
|
133
|
+
failures thrown from a custom `setContextClient`'s `query` — see
|
|
134
|
+
[Errors](../README.md#errors) in the README for a full example.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Lifecycle
|
|
2
|
+
|
|
3
|
+
How the context module decides whether to accept a new context, how it resolves one
|
|
4
|
+
automatically on startup, and how a parent/child pair of instances stay in sync.
|
|
5
|
+
|
|
6
|
+
## Setting context
|
|
7
|
+
|
|
8
|
+
`setCurrentContext(item, opt)` is the entry point behind `setCurrentContextById`,
|
|
9
|
+
`connectParentContext`, and the initial-context resolver below. It takes two options, both
|
|
10
|
+
optional and both defaulting to `true`:
|
|
11
|
+
|
|
12
|
+
- **`validate`** — check `item` against `validateContext` before accepting it.
|
|
13
|
+
- **`resolve`** — when validation fails, try to resolve a related item instead of throwing.
|
|
14
|
+
|
|
15
|
+
```mermaid
|
|
16
|
+
flowchart TD
|
|
17
|
+
callSet["setCurrentContext"] --> sameId{"same id as current context?"}
|
|
18
|
+
sameId -- "yes, unchanged" --> keepCurrent["keep current context"]
|
|
19
|
+
sameId -- "no" --> shouldValidate{"validate requested?"}
|
|
20
|
+
|
|
21
|
+
shouldValidate -- "no" --> changeEvent[["onCurrentContextChange"]]
|
|
22
|
+
changeEvent -- "canceled" --> throwChange["throw: change rejected"]
|
|
23
|
+
changeEvent -- "not canceled" --> applyContext["apply item as current context"]
|
|
24
|
+
|
|
25
|
+
shouldValidate -- "yes" --> runValidate["run validateContext"]
|
|
26
|
+
runValidate -- "valid" --> changeEvent
|
|
27
|
+
runValidate -- "invalid" --> shouldResolve{"resolve requested?"}
|
|
28
|
+
|
|
29
|
+
shouldResolve -- "no" --> throwInvalid["throw: item did not validate"]
|
|
30
|
+
shouldResolve -- "yes" --> resolveEvent[["onSetContextResolve"]]
|
|
31
|
+
resolveEvent -- "canceled" --> throwResolveCanceled["throw: resolution canceled"]
|
|
32
|
+
resolveEvent -- "not canceled" --> runResolve["run resolveContext"]
|
|
33
|
+
|
|
34
|
+
runResolve -- "failed" --> throwResolveFailed["throw: no related item resolved"]
|
|
35
|
+
runResolve -- "resolved" --> resolvedEvent[["onSetContextResolved"]]
|
|
36
|
+
resolvedEvent -- "canceled" --> throwResolvedCanceled["throw: resolved item rejected"]
|
|
37
|
+
resolvedEvent -- "not canceled" --> callSet
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
A few things worth calling out that aren't obvious from the diagram alone:
|
|
41
|
+
|
|
42
|
+
- **Setting context is queued, not immediate.** Every call to `setCurrentContext` is pushed
|
|
43
|
+
onto an internal queue and processed in order — even if nobody subscribes to the returned
|
|
44
|
+
`Observable`. This means two rapid calls (e.g. a user clicking two picker items quickly)
|
|
45
|
+
resolve one after another rather than racing.
|
|
46
|
+
- **Unsubscribing aborts the queued task.** If the caller unsubscribes from the returned
|
|
47
|
+
observable before it completes, that in-flight context change is aborted and removed from the
|
|
48
|
+
queue — it will not silently apply later.
|
|
49
|
+
- **`onCurrentContextChange` (no "d") fires *before* the context actually changes** and is
|
|
50
|
+
cancelable — a listener calling `event.preventDefault()` makes `setCurrentContext` throw
|
|
51
|
+
instead of silently no-op-ing.
|
|
52
|
+
- **`onCurrentContextChanged` (with "d") fires *after*** a change has been applied, and is
|
|
53
|
+
*not* cancelable — it's an announcement, not a gate. This is the event `connectParentContext`
|
|
54
|
+
listens for to bubble changes to a parent.
|
|
55
|
+
|
|
56
|
+
## Resolving context
|
|
57
|
+
|
|
58
|
+
When validation fails and `opt.resolve` is `true`, the provider looks for a related item of an
|
|
59
|
+
accepted type instead of rejecting the item outright:
|
|
60
|
+
|
|
61
|
+
```mermaid
|
|
62
|
+
flowchart LR
|
|
63
|
+
start["run resolveContext"] --> related["fetch relatedContexts for item"]
|
|
64
|
+
related --> filterValid["keep items where validateContext passes"]
|
|
65
|
+
filterValid -- "one or more found" --> firstMatch["return the first matching item"]
|
|
66
|
+
filterValid -- "none found" --> fail["throw: no related item found"]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
> [!NOTE]
|
|
70
|
+
> If no `contextType` allow-list is configured (`setContextType`), every context item
|
|
71
|
+
> validates, so resolution never has anything left to correct — see
|
|
72
|
+
> [Configuration](../README.md#configuration).
|
|
73
|
+
|
|
74
|
+
The default `resolveContext` implementation can be replaced entirely with `setResolveContext`
|
|
75
|
+
when the "look at related items of an accepted type" strategy doesn't fit — see the
|
|
76
|
+
[Configuration reference](../README.md#configuration-reference) in the README.
|
|
77
|
+
|
|
78
|
+
## Resolving the initial context
|
|
79
|
+
|
|
80
|
+
When a context module instance is initialized, it tries — without any application code asking
|
|
81
|
+
it to — to figure out what its current context should be, in this order:
|
|
82
|
+
|
|
83
|
+
1. **From the URL.** If this instance (or its parent, when nested) has a `navigation` module,
|
|
84
|
+
its current path is run through `extractContextIdFromPath` (a GUID matcher by default). If an
|
|
85
|
+
ID is found, that item is fetched via the context client's `get` and used as a candidate.
|
|
86
|
+
2. **From the parent context.** If step 1 doesn't produce anything — no navigation module, no
|
|
87
|
+
ID in the path, or no matching item — the instance falls back to its parent context module's
|
|
88
|
+
*current* context, if it has one.
|
|
89
|
+
3. Whichever of the two resolves first is set as the current context, with both
|
|
90
|
+
`validate: true` and `resolve: true` — so an ID found in the URL still has to pass
|
|
91
|
+
`validateContext` (and can fall through to `resolveContext`) before it "sticks".
|
|
92
|
+
|
|
93
|
+
```mermaid
|
|
94
|
+
flowchart TD
|
|
95
|
+
init["module initializes"] --> tryPath{"path available and id extracted?"}
|
|
96
|
+
tryPath -- "yes" --> fetchById["fetch context item by id"]
|
|
97
|
+
fetchById -- "found" --> apply["set current context, validate and resolve"]
|
|
98
|
+
fetchById -- "not found or errors" --> tryParent
|
|
99
|
+
tryPath -- "no" --> tryParent{"parent has a current context?"}
|
|
100
|
+
tryParent -- "yes" --> apply
|
|
101
|
+
tryParent -- "no" --> none["current context stays unresolved"]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
A portal opened directly at `/apps/my-app/7fd97952-...` resolves its context straight from that
|
|
105
|
+
GUID; an app embedded inside a portal with no context in its own URL instead inherits whatever
|
|
106
|
+
project the portal already has selected.
|
|
107
|
+
|
|
108
|
+
Replace this entire strategy with `setResolveInitialContext` when neither source fits — for
|
|
109
|
+
example, an application that always derives its context from a non-GUID slug, or one that must
|
|
110
|
+
never auto-adopt a parent's context. Failures here are caught and logged as a warning rather
|
|
111
|
+
than thrown, so a broken initial-context lookup doesn't block the rest of module
|
|
112
|
+
initialization.
|
|
113
|
+
|
|
114
|
+
## Parent/child propagation
|
|
115
|
+
|
|
116
|
+
Every context module instance observes its nearest ancestor's current context and mirrors it
|
|
117
|
+
locally, unless a listener opts out. This is what keeps a portal's selected project and an
|
|
118
|
+
embedded application's context in sync without either side polling the other — set once in the
|
|
119
|
+
portal, and it propagates down; a validated change from the app can bubble back up.
|
|
120
|
+
|
|
121
|
+
```mermaid
|
|
122
|
+
flowchart TD
|
|
123
|
+
subgraph parent["parent instance, e.g. a portal"]
|
|
124
|
+
parentSet["setCurrentContext is called"]
|
|
125
|
+
parentObserve["listens for onCurrentContextChanged"] --> sourceCheck{"event source is a different provider?"}
|
|
126
|
+
sourceCheck -- "yes" --> parentSet
|
|
127
|
+
end
|
|
128
|
+
subgraph child["child instance, e.g. an embedded app"]
|
|
129
|
+
childSubscribe["subscribes to parent currentContext stream"] --> parentChangedEvent[["onParentContextChanged"]]
|
|
130
|
+
parentChangedEvent -- "not canceled" --> childValidate["validate context"]
|
|
131
|
+
childValidate -- "fails" --> childResolve["resolve context"]
|
|
132
|
+
childValidate -- "passes" --> childChanged[["onCurrentContextChanged"]]
|
|
133
|
+
childResolve -- "resolved" --> childChanged
|
|
134
|
+
childResolve -- "fails" --> childNone["child keeps no current context"]
|
|
135
|
+
end
|
|
136
|
+
parentSet -.->|"currentContext stream emits"| childSubscribe
|
|
137
|
+
childChanged -- "not stopped" --> parentObserve
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- A listener calls `event.stopPropagation()` on `onCurrentContextChanged` to keep a context
|
|
141
|
+
change local, so it never reaches ancestors or siblings.
|
|
142
|
+
- A listener calls `event.preventDefault()` on `onParentContextChanged` to reject an incoming
|
|
143
|
+
context change from a parent instead of mirroring it.
|
|
144
|
+
- A child that fails to validate *and* resolve a parent's context is left with **no current
|
|
145
|
+
context** — the parent's context is never force-applied. This is deliberate: an app that only
|
|
146
|
+
understands `'Facility'` context shouldn't be silently handed a `'Contract'` it can't use.
|
|
147
|
+
- Only the first parent-context emission after connecting can be skipped
|
|
148
|
+
(`connectParentContext(provider, { skipFirst: true })`), and even without that option, a
|
|
149
|
+
context that already matches the child's current context by `id` is ignored — so a child
|
|
150
|
+
reconnecting to a parent it's already in sync with does not re-trigger validation.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
// constrain a context change to this instance only, never bubbling to ancestors
|
|
154
|
+
modules.event.addEventListener('onCurrentContextChanged', (event) => {
|
|
155
|
+
if (event.source === modules.context) {
|
|
156
|
+
event.stopPropagation();
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
See [Events](../README.md#events) in the README for the full list of dispatched events and
|
|
162
|
+
whether each is cancelable, and [Data model](data-model.md) for what a context item and its
|
|
163
|
+
`type` actually look like.
|
package/docs/recipes.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Recipes
|
|
2
|
+
|
|
3
|
+
Configuration patterns that go beyond the [Configuration reference](../README.md#configuration-reference)
|
|
4
|
+
in the README.
|
|
5
|
+
|
|
6
|
+
## OData query parameters
|
|
7
|
+
|
|
8
|
+
`setContextParameterFn` maps a search + type into the parameters `setContextClient`'s `query`
|
|
9
|
+
receives. The default shape is a plain `{ search, filter: { type } }` object, but a context API
|
|
10
|
+
backed by OData can build the filter with [`odata-query`](https://www.npmjs.com/package/odata-query)
|
|
11
|
+
instead:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import buildQuery from 'odata-query';
|
|
15
|
+
|
|
16
|
+
enableContext(configurator, (builder) => {
|
|
17
|
+
builder.setContextParameterFn(({ search, type }) =>
|
|
18
|
+
buildQuery({
|
|
19
|
+
search,
|
|
20
|
+
filter: { type: { in: type } },
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Rewriting a path on context change
|
|
27
|
+
|
|
28
|
+
`setContextPathGenerator` builds the URL a navigation module pushes to when the current
|
|
29
|
+
context changes. Beyond a simple GUID swap, a path can also carry an application-specific key
|
|
30
|
+
derived from the resolved item:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
enableContext(configurator, (builder) => {
|
|
34
|
+
builder.setContextPathGenerator((item, path) => {
|
|
35
|
+
// /app/old-app-key/overview -> /app/new-app-key/overview
|
|
36
|
+
return path.replace(/^(\/)?app\/[^/]+(.*)$/, `/app/${item.value.appKey}$2`);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Pair this with a matching `setContextPathExtractor` so the same key round-trips back into a
|
|
42
|
+
context id on page load — see [setContextPathExtractor](../README.md#configuration-reference)
|
|
43
|
+
in the README.
|
|
44
|
+
|
|
45
|
+
## Accepting a family of related context types
|
|
46
|
+
|
|
47
|
+
`setContextType` matches exact type IDs, so an allow-list of `['ProjectMaster']` rejects a
|
|
48
|
+
`'Facility'` item even if that facility's [`ContextItemType.parentTypeIds`](data-model.md#contextitemtype-and-hierarchies)
|
|
49
|
+
includes `'ProjectMaster'`. To accept a type *and* its declared children, widen validation
|
|
50
|
+
instead of the allow-list:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
enableContext(configurator, (builder) => {
|
|
54
|
+
const acceptedTypes = ['ProjectMaster'];
|
|
55
|
+
|
|
56
|
+
builder.setContextType(acceptedTypes);
|
|
57
|
+
builder.setValidateContext((item) => {
|
|
58
|
+
if (acceptedTypes.includes(item.type.id)) return true;
|
|
59
|
+
return Boolean(item.type.isChildType && item.type.parentTypeIds?.some((id) => acceptedTypes.includes(id)));
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This keeps `setContextType` as the source of truth for the query allow-list (so searches still
|
|
65
|
+
scope to the right types server-side) while letting validation reason about the type hierarchy.
|
|
66
|
+
|
|
67
|
+
## Skipping the default initial-context lookup
|
|
68
|
+
|
|
69
|
+
By default, a newly initialized instance tries to resolve its context from the URL, then from
|
|
70
|
+
its parent — see [Resolving the initial context](lifecycle.md#resolving-the-initial-context).
|
|
71
|
+
An application that manages its own startup context entirely (e.g. from application state
|
|
72
|
+
rather than the URL or a parent) can replace that lookup outright:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { EMPTY } from 'rxjs';
|
|
76
|
+
|
|
77
|
+
enableContext(configurator, (builder) => {
|
|
78
|
+
// never auto-resolve an initial context; the application sets one explicitly later
|
|
79
|
+
builder.setResolveInitialContext(() => EMPTY);
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Custom search errors
|
|
84
|
+
|
|
85
|
+
`FusionContextSearchError`, thrown from `setContextClient`'s `query`, lets an application
|
|
86
|
+
surface a domain-specific search failure instead of a generic error — see the
|
|
87
|
+
[app-react-context-custom-error](https://github.com/equinor/fusion-framework/tree/main/cookbooks/app-react-context-custom-error/src/config.ts)
|
|
88
|
+
cookbook for a complete example.
|
|
89
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@equinor/fusion-framework-module-context",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0-next.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./dist/esm/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
"import": "./dist/esm/utils/index.js",
|
|
17
17
|
"types": "./dist/types/utils/index.d.ts"
|
|
18
18
|
},
|
|
19
|
+
"./mock": {
|
|
20
|
+
"import": "./dist/esm/mock/index.js",
|
|
21
|
+
"types": "./dist/types/mock/index.d.ts"
|
|
22
|
+
},
|
|
23
|
+
"./mock/fixtures": {
|
|
24
|
+
"import": "./dist/esm/mock/fixtures/index.js",
|
|
25
|
+
"types": "./dist/types/mock/fixtures/index.d.ts"
|
|
26
|
+
},
|
|
19
27
|
"./package.json": "./package.json"
|
|
20
28
|
},
|
|
21
29
|
"types": "./dist/types/index.d.ts",
|
|
@@ -29,6 +37,12 @@
|
|
|
29
37
|
],
|
|
30
38
|
"utils": [
|
|
31
39
|
"dist/types/utils/index.d.ts"
|
|
40
|
+
],
|
|
41
|
+
"mock": [
|
|
42
|
+
"dist/types/mock/index.d.ts"
|
|
43
|
+
],
|
|
44
|
+
"mock/fixtures": [
|
|
45
|
+
"dist/types/mock/fixtures/index.d.ts"
|
|
32
46
|
]
|
|
33
47
|
}
|
|
34
48
|
},
|
|
@@ -45,25 +59,39 @@
|
|
|
45
59
|
},
|
|
46
60
|
"dependencies": {
|
|
47
61
|
"fast-deep-equal": "^3.1.3",
|
|
48
|
-
"@equinor/fusion-query": "^7.0.
|
|
62
|
+
"@equinor/fusion-query": "^7.0.4-next.0"
|
|
49
63
|
},
|
|
50
64
|
"devDependencies": {
|
|
65
|
+
"@faker-js/faker": "^10.1.0",
|
|
51
66
|
"rxjs": "^7.8.1",
|
|
52
67
|
"typescript": "^7.0.2",
|
|
53
68
|
"@types/semver": "^7.0.0",
|
|
54
69
|
"semver": "^7.0.0",
|
|
55
|
-
"@equinor/fusion-framework-module
|
|
56
|
-
"@equinor/fusion-framework-module-
|
|
57
|
-
"@equinor/fusion-framework-module": "^
|
|
58
|
-
"@equinor/fusion-framework-module-
|
|
70
|
+
"@equinor/fusion-framework-module": "^6.1.3-next.0",
|
|
71
|
+
"@equinor/fusion-framework-module-event": "^6.1.0-next.0",
|
|
72
|
+
"@equinor/fusion-framework-module-http": "^8.1.0-next.0",
|
|
73
|
+
"@equinor/fusion-framework-module-navigation": "^7.0.8-next.0",
|
|
74
|
+
"@equinor/fusion-framework-module-services": "^8.1.1-next.0",
|
|
75
|
+
"@equinor/fusion-framework-module-telemetry": "^8.0.0-next.0"
|
|
59
76
|
},
|
|
60
77
|
"peerDependencies": {
|
|
78
|
+
"@faker-js/faker": "^10.1.0",
|
|
61
79
|
"@types/semver": "^7.0.0",
|
|
62
80
|
"semver": "^7.0.0",
|
|
63
81
|
"rxjs": "^7.0.0",
|
|
64
|
-
"@equinor/fusion-framework-module": "^6.1.
|
|
82
|
+
"@equinor/fusion-framework-module": "^6.1.3-next.0",
|
|
83
|
+
"@equinor/fusion-framework-module-telemetry": "^8.0.0-next.0"
|
|
84
|
+
},
|
|
85
|
+
"peerDependenciesMeta": {
|
|
86
|
+
"@equinor/fusion-framework-module-telemetry": {
|
|
87
|
+
"optional": true
|
|
88
|
+
},
|
|
89
|
+
"@faker-js/faker": {
|
|
90
|
+
"optional": true
|
|
91
|
+
}
|
|
65
92
|
},
|
|
66
93
|
"scripts": {
|
|
67
|
-
"build": "tsc -b"
|
|
94
|
+
"build": "tsc -b",
|
|
95
|
+
"test": "vitest run"
|
|
68
96
|
}
|
|
69
97
|
}
|