@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.
Files changed (123) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +78 -2
  3. package/dist/esm/ContextModuleConfig.js +2 -0
  4. package/dist/esm/ContextModuleConfig.js.map +1 -0
  5. package/dist/esm/ContextModuleConfigurator.interface.js +2 -0
  6. package/dist/esm/ContextModuleConfigurator.interface.js.map +1 -0
  7. package/dist/esm/ContextModuleConfigurator.js +211 -0
  8. package/dist/esm/ContextModuleConfigurator.js.map +1 -0
  9. package/dist/esm/ContextProvider.js +1 -1
  10. package/dist/esm/ContextProvider.js.map +1 -1
  11. package/dist/esm/__tests__/ContextModuleConfigurator.test.js +282 -0
  12. package/dist/esm/__tests__/ContextModuleConfigurator.test.js.map +1 -0
  13. package/dist/esm/__tests__/mock/context-mock.test.js +101 -0
  14. package/dist/esm/__tests__/mock/context-mock.test.js.map +1 -0
  15. package/dist/esm/__tests__/mock/create-context-item-factory.test.js +45 -0
  16. package/dist/esm/__tests__/mock/create-context-item-factory.test.js.map +1 -0
  17. package/dist/esm/__tests__/mock/create-context-items.test.js +46 -0
  18. package/dist/esm/__tests__/mock/create-context-items.test.js.map +1 -0
  19. package/dist/esm/index.js +1 -1
  20. package/dist/esm/index.js.map +1 -1
  21. package/dist/esm/mock/ContextMockConfigurator.js +206 -0
  22. package/dist/esm/mock/ContextMockConfigurator.js.map +1 -0
  23. package/dist/esm/mock/fixtures/create-context-item-factory.js +50 -0
  24. package/dist/esm/mock/fixtures/create-context-item-factory.js.map +1 -0
  25. package/dist/esm/mock/fixtures/create-context-items.js +53 -0
  26. package/dist/esm/mock/fixtures/create-context-items.js.map +1 -0
  27. package/dist/esm/mock/fixtures/index.js +22 -0
  28. package/dist/esm/mock/fixtures/index.js.map +1 -0
  29. package/dist/esm/mock/fixtures/string-to-seed.js +15 -0
  30. package/dist/esm/mock/fixtures/string-to-seed.js.map +1 -0
  31. package/dist/esm/mock/index.js +31 -0
  32. package/dist/esm/mock/index.js.map +1 -0
  33. package/dist/esm/mock/module.js +41 -0
  34. package/dist/esm/mock/module.js.map +1 -0
  35. package/dist/esm/module.js +35 -6
  36. package/dist/esm/module.js.map +1 -1
  37. package/dist/esm/{get-context-selector.js → selectors/get-context-selector.js} +1 -4
  38. package/dist/esm/selectors/get-context-selector.js.map +1 -0
  39. package/dist/esm/selectors/index.js +13 -0
  40. package/dist/esm/selectors/index.js.map +1 -0
  41. package/dist/esm/{query-context-selector.js → selectors/query-context-selector.js} +1 -1
  42. package/dist/esm/selectors/query-context-selector.js.map +1 -0
  43. package/dist/esm/{related-context-selector.js → selectors/related-context-selector.js} +1 -1
  44. package/dist/esm/selectors/related-context-selector.js.map +1 -0
  45. package/dist/esm/utils/enable-context.js +1 -1
  46. package/dist/esm/utils/enable-context.js.map +1 -1
  47. package/dist/esm/utils/index.js +2 -0
  48. package/dist/esm/utils/index.js.map +1 -1
  49. package/dist/esm/utils/parse-context-item.js.map +1 -0
  50. package/dist/esm/utils/resolve-initial-context.js +3 -1
  51. package/dist/esm/utils/resolve-initial-context.js.map +1 -1
  52. package/dist/esm/version.js +1 -1
  53. package/dist/esm/version.js.map +1 -1
  54. package/dist/tsconfig.tsbuildinfo +1 -1
  55. package/dist/types/{configurator.d.ts → ContextModuleConfig.d.ts} +4 -64
  56. package/dist/types/ContextModuleConfigurator.d.ts +97 -0
  57. package/dist/types/{ContextConfigBuilder.d.ts → ContextModuleConfigurator.interface.d.ts} +27 -47
  58. package/dist/types/ContextProvider.d.ts +1 -1
  59. package/dist/types/__tests__/ContextModuleConfigurator.test.d.ts +1 -0
  60. package/dist/types/__tests__/mock/context-mock.test.d.ts +1 -0
  61. package/dist/types/__tests__/mock/create-context-item-factory.test.d.ts +1 -0
  62. package/dist/types/__tests__/mock/create-context-items.test.d.ts +1 -0
  63. package/dist/types/index.d.ts +3 -1
  64. package/dist/types/mock/ContextMockConfigurator.d.ts +155 -0
  65. package/dist/types/mock/fixtures/create-context-item-factory.d.ts +40 -0
  66. package/dist/types/mock/fixtures/create-context-items.d.ts +59 -0
  67. package/dist/types/mock/fixtures/index.d.ts +21 -0
  68. package/dist/types/mock/fixtures/string-to-seed.d.ts +12 -0
  69. package/dist/types/mock/index.d.ts +30 -0
  70. package/dist/types/mock/module.d.ts +38 -0
  71. package/dist/types/module.d.ts +5 -3
  72. package/dist/types/{get-context-selector.d.ts → selectors/get-context-selector.d.ts} +1 -3
  73. package/dist/types/selectors/index.d.ts +12 -0
  74. package/dist/types/{query-context-selector.d.ts → selectors/query-context-selector.d.ts} +1 -1
  75. package/dist/types/{related-context-selector.d.ts → selectors/related-context-selector.d.ts} +1 -1
  76. package/dist/types/utils/enable-context.d.ts +4 -5
  77. package/dist/types/utils/index.d.ts +2 -0
  78. package/dist/types/{parse-context-item.d.ts → utils/parse-context-item.d.ts} +1 -1
  79. package/dist/types/utils/resolve-initial-context.d.ts +1 -1
  80. package/dist/types/version.d.ts +1 -1
  81. package/docs/data-model.md +134 -0
  82. package/docs/lifecycle.md +163 -0
  83. package/docs/recipes.md +89 -0
  84. package/package.json +36 -8
  85. package/src/ContextModuleConfig.ts +147 -0
  86. package/src/ContextModuleConfigurator.interface.ts +145 -0
  87. package/src/ContextModuleConfigurator.ts +295 -0
  88. package/src/ContextProvider.ts +1 -2
  89. package/src/__tests__/ContextModuleConfigurator.test.ts +412 -0
  90. package/src/__tests__/mock/context-mock.test.ts +137 -0
  91. package/src/__tests__/mock/create-context-item-factory.test.ts +60 -0
  92. package/src/__tests__/mock/create-context-items.test.ts +58 -0
  93. package/src/index.ts +3 -5
  94. package/src/mock/ContextMockConfigurator.ts +244 -0
  95. package/src/mock/fixtures/create-context-item-factory.ts +62 -0
  96. package/src/mock/fixtures/create-context-items.ts +80 -0
  97. package/src/mock/fixtures/index.ts +28 -0
  98. package/src/mock/fixtures/string-to-seed.ts +18 -0
  99. package/src/mock/index.ts +33 -0
  100. package/src/mock/module.ts +54 -0
  101. package/src/module.ts +45 -17
  102. package/src/{get-context-selector.ts → selectors/get-context-selector.ts} +2 -6
  103. package/src/selectors/index.ts +12 -0
  104. package/src/{query-context-selector.ts → selectors/query-context-selector.ts} +2 -2
  105. package/src/{related-context-selector.ts → selectors/related-context-selector.ts} +2 -2
  106. package/src/utils/enable-context.ts +6 -10
  107. package/src/utils/index.ts +2 -0
  108. package/src/{parse-context-item.ts → utils/parse-context-item.ts} +1 -1
  109. package/src/utils/resolve-initial-context.ts +5 -2
  110. package/src/version.ts +1 -1
  111. package/tsconfig.json +6 -0
  112. package/vitest.config.ts +11 -0
  113. package/dist/esm/ContextConfigBuilder.js +0 -175
  114. package/dist/esm/ContextConfigBuilder.js.map +0 -1
  115. package/dist/esm/configurator.js +0 -104
  116. package/dist/esm/configurator.js.map +0 -1
  117. package/dist/esm/get-context-selector.js.map +0 -1
  118. package/dist/esm/parse-context-item.js.map +0 -1
  119. package/dist/esm/query-context-selector.js.map +0 -1
  120. package/dist/esm/related-context-selector.js.map +0 -1
  121. package/src/ContextConfigBuilder.ts +0 -258
  122. package/src/configurator.ts +0 -300
  123. /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;
@@ -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 IContextModuleConfigurator } from './configurator';
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 './types';
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,4 +1,4 @@
1
- import type { ContextItem } from './types';
1
+ import type { ContextItem } from '../types';
2
2
  /**
3
3
  * Parse the response from the QueryContext API into an array of context items.
4
4
  * @param response The response object.
@@ -1,4 +1,4 @@
1
- import type { ContextItem } from './types';
1
+ import type { ContextItem } from '../types';
2
2
  /**
3
3
  * Parse the response from the RelatedContext API into an array of context items.
4
4
  * @param response The response object containing the related context items.
@@ -1,11 +1,10 @@
1
- import type { IModulesConfigurator, AnyModule, ModuleInitializerArgs } from '@equinor/fusion-framework-module';
2
- import type { IContextModuleConfigurator } from '../configurator';
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 a context configuration builder. This function can be used to further configure the context module. It can be asynchronous.
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?: <TDeps extends Array<AnyModule> = []>(builder: ContextConfigBuilder<TDeps, ModuleInitializerArgs<IContextModuleConfigurator, TDeps>>) => void | Promise<void>) => void;
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 './types';
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.
@@ -1,4 +1,4 @@
1
- import type { ContextModuleConfig } from '../configurator';
1
+ import type { ContextModuleConfig } from '../ContextModuleConfig';
2
2
  import { type ContextPathResolveArgs } from './resolve-context-from-path';
3
3
  /**
4
4
  * Resolves the initial context from the parent module.
@@ -1 +1 @@
1
- export declare const version = "8.0.3";
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.
@@ -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": "8.0.3",
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.3"
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-event": "^6.0.1",
56
- "@equinor/fusion-framework-module-navigation": "^7.0.7",
57
- "@equinor/fusion-framework-module": "^6.1.2",
58
- "@equinor/fusion-framework-module-services": "^8.1.0"
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.2"
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
  }