@uxland/primary-shell 8.0.0-rc.0 → 8.0.0-rc.1

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 (66) hide show
  1. package/dist/{component-Bt_gBs5e.js → component-BY2raxSF.js} +2 -2
  2. package/dist/{component-Bt_gBs5e.js.map → component-BY2raxSF.js.map} +1 -1
  3. package/dist/{index-8n9_rIYI.js → index-BZNo-mYO.js} +15427 -15933
  4. package/dist/index-BZNo-mYO.js.map +1 -0
  5. package/dist/index.js +1 -1
  6. package/dist/index.umd.cjs +2623 -2668
  7. package/dist/index.umd.cjs.map +1 -1
  8. package/dist/{plugin-B5E6LZ4X.js → plugin-IG9tBP6I.js} +5909 -5888
  9. package/dist/plugin-IG9tBP6I.js.map +1 -0
  10. package/dist/primary/shell/src/UI/components/clinical-pathways-action-menu/clinical-pathways-action-menu.d.ts +4 -12
  11. package/dist/primary/shell/src/UI/components/communication-action-menu/communication-action-menu.d.ts +4 -12
  12. package/dist/primary/shell/src/UI/components/petitioner-action-menu/petitioner-action-menu.d.ts +4 -12
  13. package/dist/primary/shell/src/UI/components/sidenav-action-menu/sidenav-action-menu.d.ts +27 -0
  14. package/dist/primary/shell/src/UI/components/sidenav-action-menu/template.d.ts +2 -0
  15. package/dist/primary/shell/src/UI/shared-components/primaria-text-editor/primaria-rich-text-editor.d.ts +7 -1
  16. package/dist/primary/shell/src/api/import-data-manager/component/component.d.ts +1 -0
  17. package/dist/primary/shell/src/api/region-manager/region-manager.d.ts +4 -3
  18. package/dist/primary/shell/src/api/region-manager/remove-view-idempotency.test.d.ts +1 -0
  19. package/dist/primary/shell/src/disposer.d.ts +1 -1
  20. package/dist/primary/shell/src/features/bootstrapper.d.ts +1 -1
  21. package/dist/primary/shell/src/features/exit/handler.d.ts +12 -0
  22. package/dist/primary/shell/src/features/get-user-info/backend-adapter.d.ts +4 -2
  23. package/dist/primary/shell/src/features/get-user-info/pds-token.d.ts +26 -0
  24. package/dist/primary/shell/src/features/get-visi-id/action.d.ts +2 -0
  25. package/package.json +3 -3
  26. package/src/UI/components/clinical-pathways-action-menu/clinical-pathways-action-menu.ts +6 -41
  27. package/src/UI/components/communication-action-menu/communication-action-menu.ts +6 -41
  28. package/src/UI/components/petitioner-action-menu/petitioner-action-menu.ts +6 -41
  29. package/src/UI/components/primaria-shell/primaria-shell.ts +3 -1
  30. package/src/UI/components/sidenav-action-menu/sidenav-action-menu.ts +62 -0
  31. package/src/UI/components/{petitioner-action-menu → sidenav-action-menu}/template.ts +3 -3
  32. package/src/UI/internal-views/doctor-nav-menu.ts +9 -2
  33. package/src/UI/shared-components/bootstrapper.ts +3 -1
  34. package/src/UI/shared-components/primaria-nav-item/primaria-nav-item.ts +3 -1
  35. package/src/UI/shared-components/primaria-text-editor/primaria-rich-text-editor.ts +34 -21
  36. package/src/api/activity-monitor/activity-monitor.test.ts +1 -1
  37. package/src/api/ecap-event-manager/ecap-event-manager.test.ts +12 -4
  38. package/src/api/http-client/http-client.test.ts +12 -31
  39. package/src/api/import-data-manager/component/component.ts +3 -0
  40. package/src/api/import-data-manager/import-data-manager-impl.test.ts +2 -0
  41. package/src/api/interaction-service/interaction-service-impl.tsx +19 -8
  42. package/src/api/localization/localization.ts +44 -40
  43. package/src/api/notification-service/notification-service-impl.test.ts +2 -1
  44. package/src/api/region-manager/region-manager.test.ts +59 -3
  45. package/src/api/region-manager/region-manager.ts +52 -10
  46. package/src/api/region-manager/remove-view-idempotency.test.ts +98 -0
  47. package/src/api/token-manager/token-manager.test.ts +3 -6
  48. package/src/disposer.ts +2 -2
  49. package/src/features/bootstrapper.ts +2 -2
  50. package/src/features/exit/handler.test.ts +60 -0
  51. package/src/features/exit/handler.ts +18 -2
  52. package/src/features/get-user-info/backend-adapter.ts +30 -18
  53. package/src/features/get-user-info/handler.test.ts +8 -2
  54. package/src/features/get-user-info/pds-token.ts +28 -0
  55. package/src/features/get-visi-id/action.ts +11 -0
  56. package/src/types/custom.d.ts +30 -0
  57. package/dist/index-8n9_rIYI.js.map +0 -1
  58. package/dist/plugin-B5E6LZ4X.js.map +0 -1
  59. package/dist/primary/shell/src/UI/components/clinical-pathways-action-menu/template.d.ts +0 -2
  60. package/dist/primary/shell/src/UI/components/communication-action-menu/template.d.ts +0 -2
  61. package/dist/primary/shell/src/UI/components/petitioner-action-menu/template.d.ts +0 -2
  62. package/src/UI/components/clinical-pathways-action-menu/template.ts +0 -24
  63. package/src/UI/components/communication-action-menu/styles.css +0 -29
  64. package/src/UI/components/communication-action-menu/template.ts +0 -24
  65. package/src/UI/components/petitioner-action-menu/styles.css +0 -29
  66. /package/src/UI/components/{clinical-pathways-action-menu → sidenav-action-menu}/styles.css +0 -0
@@ -0,0 +1,98 @@
1
+ // @vitest-environment happy-dom
2
+ import { createRegionManager } from "@uxland/harmonix";
3
+ import { type IRegionAdapter, Region } from "@uxland/regions";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ /**
7
+ * Aquest test NO fa servir dobles del regionManager: munta una `Region` de `@uxland/regions` i un
8
+ * `RegionManager` de `@uxland/harmonix` de debo.
9
+ *
10
+ * El motiu: des que el shell desmunta totes les vistes al tancar (`_removeAllViews`), l'
11
+ * `unregisterViews` de cada plugin s'executa DESPRES, sobre vistes que ja no hi son. Tot el
12
+ * disseny es sosté en que aquesta segona passada sigui inofensiva, i aixo depen del comportament
13
+ * intern d'aquestes dues llibreries, no del nostre codi. Amb dobles no es provaria res.
14
+ *
15
+ * Si una actualitzacio d'aquests paquets fa que la segona crida arribi a l'adaptador, el
16
+ * component es desmuntaria dues vegades i ara mateix nomes ho sabriem en produccio.
17
+ */
18
+ const REGION = "test-region";
19
+ const VIEW_KEY = "some-plugin::some-view";
20
+
21
+ let adapter: IRegionAdapter;
22
+ let regionManager: ReturnType<typeof createRegionManager>;
23
+
24
+ const createSpyAdapter = (): IRegionAdapter => ({
25
+ activateView: vi.fn().mockResolvedValue(undefined),
26
+ deactivateView: vi.fn().mockResolvedValue(undefined),
27
+ removeView: vi.fn(),
28
+ viewAdded: vi.fn().mockResolvedValue(undefined),
29
+ behaviors: [],
30
+ });
31
+
32
+ const mountRegion = () => {
33
+ const host = document.createElement("div") as HTMLElement & { uxlRegion?: unknown };
34
+ const region = new Region(REGION, regionManager as never, host as never, adapter, {
35
+ name: REGION,
36
+ targetId: REGION,
37
+ });
38
+ regionManager.add(REGION, region);
39
+ return region;
40
+ };
41
+
42
+ const addAndActivateView = async (region: Region) => {
43
+ await regionManager.registerViewWithRegion(REGION, VIEW_KEY, {
44
+ factory: () => Promise.resolve(document.createElement("span")),
45
+ });
46
+ // Nomes les vistes actives tenen component, i `removeView` nomes crida l'adaptador si n'hi ha.
47
+ await region.activate(VIEW_KEY);
48
+ };
49
+
50
+ beforeEach(() => {
51
+ adapter = createSpyAdapter();
52
+ regionManager = createRegionManager("test");
53
+ });
54
+
55
+ describe("treure una vista dues vegades (shell + plugin)", () => {
56
+ it("la primera passada desmunta la vista de debo", async () => {
57
+ const region = mountRegion();
58
+ await addAndActivateView(region);
59
+
60
+ await regionManager.unregisterViewFromRegion(REGION, VIEW_KEY);
61
+
62
+ expect(adapter.deactivateView).toHaveBeenCalledOnce();
63
+ expect(adapter.removeView).toHaveBeenCalledOnce();
64
+ });
65
+
66
+ it("la segona no torna a tocar l'adaptador", async () => {
67
+ const region = mountRegion();
68
+ await addAndActivateView(region);
69
+
70
+ await regionManager.unregisterViewFromRegion(REGION, VIEW_KEY);
71
+ vi.mocked(adapter.deactivateView).mockClear();
72
+ vi.mocked(adapter.removeView).mockClear();
73
+
74
+ // El que fa `unregisterViews` del plugin despres que el shell ja ho hagi tret tot.
75
+ await regionManager.unregisterViewFromRegion(REGION, VIEW_KEY);
76
+
77
+ expect(adapter.deactivateView).not.toHaveBeenCalled();
78
+ expect(adapter.removeView).not.toHaveBeenCalled();
79
+ });
80
+
81
+ it("la segona passada no llanca ni deixa la vista registrada", async () => {
82
+ const region = mountRegion();
83
+ await addAndActivateView(region);
84
+
85
+ await regionManager.unregisterViewFromRegion(REGION, VIEW_KEY);
86
+
87
+ await expect(regionManager.unregisterViewFromRegion(REGION, VIEW_KEY)).resolves.toBeDefined();
88
+ expect(region.containsView(VIEW_KEY)).toBe(false);
89
+ expect(regionManager.getRegisteredViews(REGION)).toEqual([]);
90
+ });
91
+
92
+ it("treure una vista que no s'ha registrat mai tampoc fa res", async () => {
93
+ mountRegion();
94
+
95
+ await expect(regionManager.unregisterViewFromRegion(REGION, "mai::registrada")).resolves.toBeDefined();
96
+ expect(adapter.removeView).not.toHaveBeenCalled();
97
+ });
98
+ });
@@ -1,3 +1,4 @@
1
+ // @vitest-environment happy-dom
1
2
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
3
  import axios from "axios";
3
4
  import { BROKER_EVENTS } from "../broker/broker-events";
@@ -68,17 +69,13 @@ describe("TokenManager", () => {
68
69
  it("should throw error if tokens are already initialized", () => {
69
70
  const tokenManager = new TokenManagerImpl(mockBroker);
70
71
  tokenManager.setInitialTokens("manual_access", "manual_refresh");
71
- expect(() => tokenManager.setInitialTokens("another_access", "another_refresh")).toThrow(
72
- "Token already initialized",
73
- );
72
+ expect(() => tokenManager.setInitialTokens("another_access", "another_refresh")).toThrow("Token already initialized");
74
73
  });
75
74
 
76
75
  it("should throw error if tokens were already initialized from URL", () => {
77
76
  const tokenManager = new TokenManagerImpl(mockBroker);
78
77
  tokenManager.getToken();
79
- expect(() => tokenManager.setInitialTokens("manual_access", "manual_refresh")).toThrow(
80
- "Token already initialized",
81
- );
78
+ expect(() => tokenManager.setInitialTokens("manual_access", "manual_refresh")).toThrow("Token already initialized");
82
79
  });
83
80
  });
84
81
 
package/src/disposer.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { activityMonitor, sessionRefreshTimer, shellApi } from "./api/api";
2
2
  import { disposeFeatures } from "./features/bootstrapper";
3
3
  import { ExitShellPayload } from "./features/exit/request";
4
- export const disposeShell = () => {
5
- disposeFeatures(shellApi);
4
+ export const disposeShell = async () => {
5
+ await disposeFeatures(shellApi);
6
6
  shellApi.interactionService.dispose();
7
7
  sessionRefreshTimer.stop();
8
8
  activityMonitor.stop();
@@ -12,10 +12,10 @@ export const useFeatures = (api: PrimariaApi) => {
12
12
  api.broker.send(new GetUserInfo());
13
13
  };
14
14
 
15
- export const disposeFeatures = (api: PrimariaApi) => {
15
+ export const disposeFeatures = async (api: PrimariaApi) => {
16
16
  disposeGetUserInfo();
17
17
  disposeExitShell();
18
18
  container.unbindAll();
19
- api.regionManager._destroy();
19
+ await api.regionManager._destroy();
20
20
  api.broker._clear();
21
21
  };
@@ -23,6 +23,9 @@ const createMockApi = (): PrimariaApi =>
23
23
  notificationService: {
24
24
  error: vi.fn(),
25
25
  },
26
+ regionManager: {
27
+ _removeAllViews: vi.fn().mockResolvedValue(undefined),
28
+ },
26
29
  }) as any;
27
30
 
28
31
  describe("ExitShellHandler", () => {
@@ -120,4 +123,61 @@ describe("ExitShellHandler", () => {
120
123
  expect(raiseCloseEvent).toHaveBeenCalled();
121
124
  expect(raiseCustomCloseEvent).not.toHaveBeenCalled();
122
125
  });
126
+
127
+ describe("ordre de tancament: vistes abans que plugins", () => {
128
+ /**
129
+ * Els cleanups dels `useEffect` s'executen en desmuntar-se el component i poden enviar
130
+ * comandes pel broker (p. ex. `ClearTransportDetail` a petitioner). Si el `dispose` dels
131
+ * plugins corre primer, el handler ja no hi es i el `send` rebutja. Cada plugin d'aquest
132
+ * repositori ja ho ordena be, pero els de tercers no els controlem: aquest ordre els cobreix.
133
+ */
134
+ it("desmunta totes les vistes abans de disposar els plugins", async () => {
135
+ const order: string[] = [];
136
+ (mockApi.regionManager._removeAllViews as any).mockImplementation(async () => {
137
+ order.push("removeAllViews");
138
+ });
139
+ (disposePlugins as any).mockImplementation(async () => {
140
+ order.push("disposePlugins");
141
+ });
142
+
143
+ await handler.handle({} as ExitShell);
144
+
145
+ expect(order).toEqual(["removeAllViews", "disposePlugins"]);
146
+ });
147
+
148
+ it("no comenca a disposar fins que el desmuntatge ha acabat", async () => {
149
+ let finishUnmount: () => void = () => {};
150
+ (mockApi.regionManager._removeAllViews as any).mockImplementation(
151
+ () =>
152
+ new Promise<void>((resolve) => {
153
+ finishUnmount = resolve;
154
+ }),
155
+ );
156
+ (disposePlugins as any).mockResolvedValue(undefined);
157
+
158
+ const handled = handler.handle({} as ExitShell);
159
+ await Promise.resolve();
160
+ await Promise.resolve();
161
+ expect(disposePlugins).not.toHaveBeenCalled();
162
+
163
+ finishUnmount();
164
+ await handled;
165
+
166
+ expect(disposePlugins).toHaveBeenCalled();
167
+ });
168
+
169
+ it("el limit de temps del tancament segueix cobrint tot el proces", async () => {
170
+ vi.useFakeTimers();
171
+ (mockApi.regionManager._removeAllViews as any).mockImplementation(() => new Promise<void>(() => {}));
172
+
173
+ const handled = handler.handle({} as ExitShell);
174
+ await vi.advanceTimersByTimeAsync(10000);
175
+ await handled;
176
+
177
+ // Encara que el desmuntatge no acabi mai, el tancament continua.
178
+ expect(disposeShell).toHaveBeenCalled();
179
+ expect(raiseCloseEvent).toHaveBeenCalled();
180
+ vi.useRealTimers();
181
+ });
182
+ });
123
183
  });
@@ -17,10 +17,10 @@ export class ExitShellHandler {
17
17
 
18
18
  // Per si un plugin tarda molt en fer dispose, màxim deixarem 5 segons, per no interrompre el tancar infinitament
19
19
  await Promise.race([
20
- disposePlugins(), // S'intenta executar un dispose normal
20
+ this.unmountViewsAndDisposePlugins(), // S'intenta executar un dispose normal
21
21
  this.timeout(10000), // Si passen 5s, es segueix amb l'execució
22
22
  ]);
23
- disposeShell();
23
+ await disposeShell();
24
24
  this.emitClose(evt);
25
25
  } catch (error) {
26
26
  this.api.notificationService.error(translate("errors.exit"));
@@ -28,6 +28,22 @@ export class ExitShellHandler {
28
28
  }
29
29
  }
30
30
 
31
+ /**
32
+ * Desmunta les vistes de tots els plugins abans de fer-los el `dispose`.
33
+ *
34
+ * Els cleanups dels `useEffect` s'executen en desmuntar-se el component i poden enviar comandes
35
+ * pel broker (p. ex. `ClearTransportDetail` a petitioner). Si el `dispose` s'executa primer, el
36
+ * handler ja no hi és i el `send` rebutja. Cada plugin d'aquest repositori ja desmunta les seves
37
+ * vistes primer, però els de tercers no els controlem: fer-ho aquí els cobreix tots.
38
+ *
39
+ * Va dins del mateix `Promise.race` que el `dispose`, així que el límit de temps del tancament
40
+ * no canvia.
41
+ */
42
+ private async unmountViewsAndDisposePlugins(): Promise<void> {
43
+ await this.api.regionManager._removeAllViews();
44
+ await disposePlugins();
45
+ }
46
+
31
47
  private timeout(ms: number) {
32
48
  return new Promise<void>((resolve) => setTimeout(resolve, ms));
33
49
  }
@@ -1,8 +1,12 @@
1
1
  import { inject, injectable } from "inversify";
2
- import { IUserInfo } from "./model";
3
2
  import { jwtDecode } from "jwt-decode";
4
- import { TYPES } from "../../infrastructure/ioc/types";
5
3
  import { shellApi } from "../../api/api";
4
+ import { TYPES } from "../../infrastructure/ioc/types";
5
+ import { IUserInfo } from "./model";
6
+ import { PdsAccessInfo, PdsToken } from "./pds-token";
7
+
8
+ /** Ordre de preferència dels identificadors del professional dins del token. */
9
+ const USER_ID_TYPES = ["DNI", "NIE", "PASS"];
6
10
 
7
11
  export abstract class UserInfoBackendAdapter {
8
12
  abstract getUserInfo(): Promise<IUserInfo>;
@@ -11,26 +15,34 @@ export abstract class UserInfoBackendAdapter {
11
15
  export class GetUserInfoBackendAdapter implements UserInfoBackendAdapter {
12
16
  constructor(@inject(TYPES.primaryApi) private api: typeof shellApi) {}
13
17
  getUserInfo(): Promise<IUserInfo> {
14
- const decodedToken = jwtDecode(this.api.tokenManager.getToken());
18
+ const decodedToken = jwtDecode<PdsToken>(this.api.tokenManager.getToken());
19
+ const accessInfo = decodedToken.access_info;
20
+
21
+ // `IUserInfo` declara `string` a tots els camps, però el token no en garanteix cap. Abans
22
+ // el token era `any` i els absents arribaven com a `undefined` amb tipus `string`; ara es
23
+ // normalitzen a cadena buida perquè el tipus digui la veritat.
15
24
  const userInfo: IUserInfo = {
16
- userId: this.getUserId(decodedToken.access_info),
17
- firstName: decodedToken.access_info.trace_user_given_name,
18
- familyName: decodedToken.access_info.trace_user_family_name,
19
- lastName: decodedToken.access_info.trace_user_last_name,
20
- registrationNumber: decodedToken.access_info.identifier.find((id: { type: string; value: string }) => id.type === "NUMCOL")
21
- ?.value,
22
- workCenter: decodedToken.access_info.center_code,
23
- role: decodedToken.access_info.role_type,
24
- speciality: decodedToken.access_info.professional_category,
25
+ userId: this.getUserId(accessInfo) ?? "",
26
+ firstName: accessInfo?.trace_user_given_name ?? "",
27
+ familyName: accessInfo?.trace_user_family_name ?? "",
28
+ lastName: accessInfo?.trace_user_last_name ?? "",
29
+ registrationNumber: this.findIdentifier(accessInfo, "NUMCOL") ?? "",
30
+ workCenter: accessInfo?.center_code ?? "",
31
+ role: accessInfo?.role_type ?? "",
32
+ speciality: accessInfo?.professional_category ?? "",
25
33
  };
26
34
  return Promise.resolve(userInfo);
27
35
  }
28
36
 
29
- getUserId(access_info) {
30
- return (
31
- access_info.identifier?.find((identifierItem: { type: string; value: string }) => identifierItem.type === "DNI")?.value ||
32
- access_info.identifier?.find((identifierItem: { type: string; value: string }) => identifierItem.type === "NIE")?.value ||
33
- access_info.identifier?.find((identifierItem: { type: string; value: string }) => identifierItem.type === "PASS")?.value
34
- );
37
+ private findIdentifier(accessInfo: PdsAccessInfo | undefined, type: string) {
38
+ return accessInfo?.identifier?.find((identifier) => identifier.type === type)?.value;
39
+ }
40
+
41
+ getUserId(accessInfo: PdsAccessInfo | undefined) {
42
+ for (const type of USER_ID_TYPES) {
43
+ const value = this.findIdentifier(accessInfo, type);
44
+ if (value) return value;
45
+ }
46
+ return undefined;
35
47
  }
36
48
  }
@@ -27,8 +27,14 @@ describe("GetUserInfoHandler", () => {
27
27
 
28
28
  const message = {} as GetUserInfo;
29
29
  const mockUserInfo: IUserInfo = {
30
- name: "John Doe",
31
- email: "john@example.com",
30
+ userId: "12345678Z",
31
+ firstName: "John",
32
+ familyName: "Doe",
33
+ lastName: "Smith",
34
+ registrationNumber: "081234",
35
+ workCenter: "CAP",
36
+ role: "assist",
37
+ speciality: "MF",
32
38
  };
33
39
 
34
40
  beforeEach(() => {
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Els camps del token del PDS que aquest adaptador llegeix.
3
+ *
4
+ * Es declara aquí, i no s'importa `PDSToken` de `@pds/core`, per dues raons: el shell no depèn
5
+ * d'aquell package, i el seu `AccessInfo` no inclou `trace_user_last_name`, que sí que es fa
6
+ * servir. Si algun dia s'unifiquen els dos contractes, aquest fitxer hauria de desaparèixer.
7
+ *
8
+ * Tot és opcional perquè el token no en garanteix cap: fins ara el tipus era `any` i
9
+ * `IUserInfo` prometia `string` a tot arreu, cosa que no era certa en runtime.
10
+ */
11
+ export interface PdsAccessInfo {
12
+ trace_user_given_name?: string;
13
+ trace_user_family_name?: string;
14
+ trace_user_last_name?: string;
15
+ center_code?: string;
16
+ role_type?: string;
17
+ professional_category?: string;
18
+ identifier?: PdsIdentifier[];
19
+ }
20
+
21
+ export interface PdsIdentifier {
22
+ type: string;
23
+ value: string;
24
+ }
25
+
26
+ export interface PdsToken {
27
+ access_info?: PdsAccessInfo;
28
+ }
@@ -0,0 +1,11 @@
1
+ import { PrimariaApi } from "../../api/api";
2
+
3
+ export const getVisiId = async (api: PrimariaApi): Promise<string | undefined> => {
4
+ const context = api.contextManager.getContext();
5
+ if (context.visi_id) return context.visi_id as string;
6
+ try {
7
+ return (await api.broker.send("get_visi_id")) as string;
8
+ } catch {
9
+ return undefined;
10
+ }
11
+ };
@@ -0,0 +1,30 @@
1
+ import "react";
2
+
3
+ /**
4
+ * Els components web del Design System de Salut (`dss-*`) no porten tipus, així que s'obre
5
+ * `JSX.IntrinsicElements` per acceptar qualsevol element personalitzat.
6
+ *
7
+ * S'amplien els dos especificadors: amb `jsx: "react-jsx"`, la cerca del namespace JSX passa per
8
+ * `react/jsx-runtime`, que hereta de `React.JSX`.
9
+ *
10
+ * Aquest fitxer és idèntic a tots els plugins i no es pot compartir des de l'arrel: sota pnpm,
11
+ * l'arrel no té `react` a `node_modules`, així que l'especificador no resoldria i el
12
+ * `declare module` no ampliaria res.
13
+ */
14
+ declare module "react" {
15
+ namespace JSX {
16
+ interface IntrinsicElements {
17
+ // biome-ignore lint/suspicious/noExplicitAny: els components del DSS no estan tipats
18
+ [key: string]: any;
19
+ }
20
+ }
21
+ }
22
+
23
+ declare module "react/jsx-runtime" {
24
+ namespace JSX {
25
+ interface IntrinsicElements {
26
+ // biome-ignore lint/suspicious/noExplicitAny: els components del DSS no estan tipats
27
+ [key: string]: any;
28
+ }
29
+ }
30
+ }