@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
@@ -7,6 +7,31 @@ import { fixSpellCheckerIssue } from "./utils";
7
7
 
8
8
  fixSpellCheckerIssue(Quill);
9
9
 
10
+ /**
11
+ * Una opció de la barra d'eines de Quill: el nom solt (`"bold"`), un objecte de configuració
12
+ * (`{ header: 1 }`) o un grup d'opcions, que és com Quill separa els blocs de la barra.
13
+ */
14
+ type ToolbarOption = string | Record<string, unknown> | (string | Record<string, unknown>)[];
15
+
16
+ /**
17
+ * Opcions de la barra d'eines que Quill espera com a objecte de configuració en lloc del nom
18
+ * solt. Abans era una cadena d'`else if`.
19
+ */
20
+ const TOOLBAR_OPTION_ALIASES: Record<string, ToolbarOption> = {
21
+ color: { color: [] },
22
+ ol: { list: "ordered" },
23
+ ul: { list: "bullet" },
24
+ subindex: { script: "sub" },
25
+ superindex: { script: "super" },
26
+ // outdent: { indent: "-1" },
27
+ // indent: { indent: "+1" },
28
+ size: { size: ["small", false, "large", "huge"] },
29
+ header: { header: [1, 2, 3, 4, 5, 6, false] },
30
+ background: { background: [] },
31
+ font: { font: [] },
32
+ align: { align: [] },
33
+ };
34
+
10
35
  export class PrimariaRichTextEditor extends LitElement {
11
36
  private textChangeTimeout: number | null = null;
12
37
 
@@ -124,7 +149,8 @@ export class PrimariaRichTextEditor extends LitElement {
124
149
  formats: string;
125
150
 
126
151
  _getOptions() {
127
- let toolbarOptions = [];
152
+ // Sense anotar, `[]` s'infereix com a `never[]` i totes les assignacions posteriors fallen.
153
+ let toolbarOptions: ToolbarOption[] = [];
128
154
  const availableOpts = this.availableOptions;
129
155
  if (this.options === undefined || this.options.length === 0) {
130
156
  toolbarOptions = [
@@ -145,26 +171,13 @@ export class PrimariaRichTextEditor extends LitElement {
145
171
  // ["clean"],
146
172
  ];
147
173
  } else {
148
- toolbarOptions = this.options.split(",");
149
- toolbarOptions.forEach((option) => {
150
- if (availableOpts.indexOf(option) <= -1) toolbarOptions.splice(toolbarOptions.indexOf(option), 1);
151
- });
152
- toolbarOptions = toolbarOptions.map(function (option) {
153
- if (option == "color") return { color: [] };
154
- else if (option == "ol") return { list: "ordered" };
155
- else if (option == "ul") return { list: "bullet" };
156
- else if (option == "subindex") return { script: "sub" };
157
- else if (option == "superindex") return { script: "super" };
158
- // else if (option == 'outdent') return {indent: '-1'};
159
- // else if (option == 'indent') return {indent: '+1'};
160
- else if (option == "size") return { size: ["small", false, "large", "huge"] };
161
- else if (option == "header") return { header: [1, 2, 3, 4, 5, 6, false] };
162
- else if (option == "background") return { background: [] };
163
- else if (option == "font") return { font: [] };
164
- else if (option == "align") return { align: [] };
165
- /* else if (option == 'undo') return {undo: undo_icon};
166
- else if (option == 'redo') return {redo: redo_icon}; */ else return option;
167
- });
174
+ // Abans això filtrava amb un `splice` dins d'un `forEach`, que salta elements quan n'hi ha
175
+ // dos d'invàlids seguits. Amb `filter` el resultat és el que la intenció demanava.
176
+ toolbarOptions = this.options
177
+ .split(",")
178
+ .filter((option) => availableOpts.indexOf(option) > -1)
179
+ // Les opcions amb configuració es tradueixen; la resta passen tal com són.
180
+ .map((option) => TOOLBAR_OPTION_ALIASES[option] ?? option);
168
181
  // if (toolbarOptions.indexOf("clean") < 0) {
169
182
  // toolbarOptions.push("clean");
170
183
  // }
@@ -1,3 +1,4 @@
1
+ // @vitest-environment happy-dom
1
2
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
3
  import { ActivityMonitorImpl } from "./activity-monitor";
3
4
 
@@ -63,5 +64,4 @@ describe("ActivityMonitor", () => {
63
64
 
64
65
  expect(monitor.getLastActivityTimestamp()).toBe(timestampBeforeStop);
65
66
  });
66
-
67
67
  });
@@ -1,3 +1,4 @@
1
+ // @vitest-environment happy-dom
1
2
  import { describe, it, expect, vi, beforeEach } from "vitest";
2
3
  import { createEcapEventManager, LAST_WORKED_DIAGNOSTICS_KEY } from "./ecap-event-manager";
3
4
 
@@ -8,6 +9,13 @@ const createMockGlobalStateManager = (data: Record<string, unknown> = {}) =>
8
9
  clearData: vi.fn(),
9
10
  }) as any;
10
11
 
12
+ /** `mock.calls[0][0]` és `unknown | undefined` amb `noUncheckedIndexedAccess`. */
13
+ const firstPostedMessage = (spy: { mock: { calls: unknown[][] } }): string => {
14
+ const payload = spy.mock.calls[0]?.[0];
15
+ if (typeof payload !== "string") throw new Error("s'esperava un missatge publicat");
16
+ return payload;
17
+ };
18
+
11
19
  describe("EcapEventManagerImpl", () => {
12
20
  const mockPostMessage = vi.fn();
13
21
  const mockConsoleLog = vi.fn();
@@ -31,7 +39,7 @@ describe("EcapEventManagerImpl", () => {
31
39
 
32
40
  eventManager.publish("click", "navigate", payload);
33
41
 
34
- const postedEvent = JSON.parse(mockPostMessage.mock.calls[0][0]);
42
+ const postedEvent = JSON.parse(firstPostedMessage(mockPostMessage));
35
43
  expect(postedEvent.DIAGNOSTICS).toBeUndefined();
36
44
  expect(globalStateManager.getData).not.toHaveBeenCalled();
37
45
  });
@@ -43,7 +51,7 @@ describe("EcapEventManagerImpl", () => {
43
51
 
44
52
  eventManager.publish("PRESCRIPCIO_ACTIVA", "NO_TANCAR", {});
45
53
 
46
- const postedEvent = JSON.parse(mockPostMessage.mock.calls[0][0]);
54
+ const postedEvent = JSON.parse(firstPostedMessage(mockPostMessage));
47
55
  expect(postedEvent.DIAGNOSTICS).toEqual(diagnostics);
48
56
  expect(globalStateManager.getData).toHaveBeenCalledWith(LAST_WORKED_DIAGNOSTICS_KEY);
49
57
  });
@@ -63,7 +71,7 @@ describe("EcapEventManagerImpl", () => {
63
71
 
64
72
  eventManager.publish(eventType, "NO_TANCAR", {});
65
73
 
66
- const postedEvent = JSON.parse(mockPostMessage.mock.calls[0][0]);
74
+ const postedEvent = JSON.parse(firstPostedMessage(mockPostMessage));
67
75
  expect(postedEvent.DIAGNOSTICS).toEqual(diagnostics);
68
76
  });
69
77
 
@@ -73,7 +81,7 @@ describe("EcapEventManagerImpl", () => {
73
81
 
74
82
  eventManager.publish("ORDRE_TRACTAMENT", "NO_TANCAR", {});
75
83
 
76
- const postedEvent = JSON.parse(mockPostMessage.mock.calls[0][0]);
84
+ const postedEvent = JSON.parse(firstPostedMessage(mockPostMessage));
77
85
  expect(postedEvent.DIAGNOSTICS).toEqual([]);
78
86
  });
79
87
 
@@ -1,3 +1,4 @@
1
+ // @vitest-environment happy-dom
1
2
  import axios, { AxiosInstance } from "axios";
2
3
  import AxiosMockAdapter from "axios-mock-adapter";
3
4
  import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -14,7 +15,7 @@ const newAccessToken =
14
15
  "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJidFZxNnRMWGpmcXdzbm5MR2FXMXdhdU9McDNiTmY4bWZ3Rm1SZ0lBS2VJIn0.eyJleHAiOjE3NDI5MTAzNzgsImlhdCI6MTc0MjkwOTQ3OCwianRpIjoiMTg4OTE3ZDYtNmM0OS00YWY0LTlhYjItZjEzYTM4MGY5OTgyIiwiaXNzIjoiaHR0cHM6Ly9wcmVwcm9kdWNjaW8ucGRzLmhlcy5jYXRzYWx1dC5nZW5jYXQuY2F0L2F1dGgvcmVhbG1zL0hFUyIsInN1YiI6Ijk1OGUyZWQ5LTJkNmUtNDZjOC1hOTE5LTU5MDQ0ZTYwMzVjMiIsInR5cCI6IkJlYXJlciIsImF6cCI6ImV0Yy1jY2YtcHJlIiwic2Vzc2lvbl9zdGF0ZSI6IjY5ODdhMzExLWM1NzQtNDc1Ni1hMzY0LWY1ZjQzZmViZjNhYiIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLWhlcyIsIlJPTEVfSEVTX0VUQyIsIlJPTEVfSEVTX0xBTkRJTkciXX0sInNjb3BlIjoiIiwic2lkIjoiNjk4N2EzMTEtYzU3NC00NzU2LWEzNjQtZjVmNDNmZWJmM2FiIiwiY2xpZW50SG9zdCI6IjEwLjUzLjI1NC4xNTAiLCJhY2Nlc3NfcnVsZXMiOnt9LCJhY2Nlc3NfaW5mbyI6eyJtb2R1bGVfY29kZSI6IkEwMDEiLCJyb2xlX3R5cGUiOiJOT1JNIiwidHJhY2VfdXNlcl9pZCI6IlVzZXJfSUQiLCJjZW50ZXJfdHlwZSI6IkUiLCJtcGlfcGF0aWVudF9pZCI6IjVlYzc0YzdiLTVhYTUtNGE4NC1iM2I0LWYwMmVkZGZkZjZkYyIsInVwX2NvZGUiOiIwNzczMyIsInRyYWNlX3VzZXJfZ2l2ZW5fbmFtZSI6IkdpdmVuIE5hbWUiLCJ0cmFjZV91c2VyX2ZhbWlseV9uYW1lIjoiRmFtaWx5IE5hbWUiLCJ1c2VyX3R5cGUiOiJBRE0iLCJjZW50ZXJfY29kZSI6IkUwODU4Njk2MyIsInByb2Zlc3Npb25hbF9jYXRlZ29yeSI6IjMwOTM0MzAwNiIsInNlcnZpY2VfY29kZSI6IjVTMDg5IiwiZXBfY29kZSI6IjAyMDgiLCJpZGVudGlmaWVyIjpbeyJ0eXBlIjoiRE5JIiwidmFsdWUiOiI3MzI4ODIxOUEifSx7InR5cGUiOiJOVU1DT0wiLCJ2YWx1ZSI6IjIifV0sImFsdF9pZGVudGlmaWVyIjpbeyJ0eXBlIjoiTVBJIiwidmFsdWUiOiIwNjIxY2Y3ZC03ZDYzLTQ5ZWMtODAwNi04YzA1NjkyZWQzNzcifV19LCJjbGllbnRBZGRyZXNzIjoiMTAuNTMuMjU0LjE1MCIsImNsaWVudF9pZCI6ImV0Yy1jY2YtcHJlIn0.zPxoN5d8_uxdbxeH7ly9sQdlnDxNp8_eWDzNSzjzkUPsRVkLxgjQw_WcjZfns-LNgGr0FToS6uSDIC7uj2hPpKqD7HPWbI6UPa7g-43Jf9rh-KSOXEHJWhlASi2w0n3xQHFSxHDYBwhyXlvWQ_Tr1Vh37MroWmEL84fdwqhcfZHmD9Ek52EuRRrB-XkymCmN1ZvO2GPU04LbBvQxLVzdAaKRzr1cUqHTnsEdgRduyKShPcyth01L_X7m128SIwjJsa1lYxFE_DbSDKz8dPTsxoS1DgwRxeArrkMlIdTHyxmDRezZUGhamyLuWpXLDXJCFfrTUB8-j_JnVlILHeR6O";
15
16
 
16
17
  describe("HTTP Client", () => {
17
- let broker = createBroker();
18
+ const broker = createBroker();
18
19
  let tokenManager: TokenManager;
19
20
  let axiosInstance: AxiosInstance;
20
21
  let axiosMockInstance: AxiosMockAdapter;
@@ -43,9 +44,7 @@ describe("HTTP Client", () => {
43
44
  expect(response.data).toBe("success");
44
45
  expect(axiosMockInstance.history.get?.length).toBe(1);
45
46
  // @ts-ignore
46
- expect(axiosMockInstance.history.get?.[0]?.headers.Authorization).toBe(
47
- `Bearer ${access_token}`,
48
- );
47
+ expect(axiosMockInstance.history.get?.[0]?.headers.Authorization).toBe(`Bearer ${access_token}`);
49
48
  });
50
49
 
51
50
  it("should retry request with new token if failed with 401 after token refresh, and requests after that have new token", async () => {
@@ -71,12 +70,8 @@ describe("HTTP Client", () => {
71
70
  expect(response.status).toBe(200);
72
71
  expect(response.data).toBe("success");
73
72
  expect(axiosMockInstance.history.get?.length).toBe(3); // 3 Calls 2 for the first request (request and retry), 1 for the second request
74
- expect(axiosMockInstance.history.get?.[1]?.headers?.Authorization).toBe(
75
- "Bearer " + newAccessToken,
76
- );
77
- expect(axiosMockInstance.history.get?.[2]?.headers?.Authorization).toBe(
78
- "Bearer " + newAccessToken,
79
- );
73
+ expect(axiosMockInstance.history.get?.[1]?.headers?.Authorization).toBe(`Bearer ${newAccessToken}`);
74
+ expect(axiosMockInstance.history.get?.[2]?.headers?.Authorization).toBe(`Bearer ${newAccessToken}`);
80
75
  });
81
76
 
82
77
  it("should fail request if token refresh fails and publish an event", async () => {
@@ -86,9 +81,7 @@ describe("HTTP Client", () => {
86
81
  const brokerSpy = vi.spyOn(broker, "publish");
87
82
  axiosMockInstance.onGet("/api/clinical-course").replyOnce(401, "Unauthorized");
88
83
 
89
- await expect(axiosInstance.get("/api/clinical-course")).rejects.toThrow(
90
- "Request failed with status code 401",
91
- );
84
+ await expect(axiosInstance.get("/api/clinical-course")).rejects.toThrow("Request failed with status code 401");
92
85
 
93
86
  expect(axiosMockInstance.history.get?.length).toBe(1); // No retry happened
94
87
  expect(brokerSpy).toHaveBeenCalledWith(BROKER_EVENTS.shell.refreshTokenFailed, {});
@@ -121,9 +114,7 @@ describe("HTTP Client", () => {
121
114
  const brokerSpy = vi.spyOn(broker, "publish");
122
115
 
123
116
  // Perform the request
124
- await expect(axiosInstance.get("/api/clinical-course")).rejects.toThrow(
125
- "Mpid header is missing",
126
- );
117
+ await expect(axiosInstance.get("/api/clinical-course")).rejects.toThrow("Mpid header is missing");
127
118
  expect(brokerSpy).not.toHaveBeenCalled();
128
119
  });
129
120
 
@@ -133,9 +124,7 @@ describe("HTTP Client", () => {
133
124
  });
134
125
  const brokerSpy = vi.spyOn(broker, "publish");
135
126
  // Perform the request
136
- await expect(axiosInstance.get("/api/clinical-course")).rejects.toThrow(
137
- "Mpid header value is invalid",
138
- );
127
+ await expect(axiosInstance.get("/api/clinical-course")).rejects.toThrow("Mpid header value is invalid");
139
128
  expect(brokerSpy).toHaveBeenCalledWith(BROKER_EVENTS.shell.mpidHeaderInvalid, expect.any(Object));
140
129
  });
141
130
 
@@ -148,9 +137,7 @@ describe("HTTP Client", () => {
148
137
  expect(response.data).toBe("success");
149
138
  expect(axiosMockInstance.history.get?.length).toBe(1);
150
139
  // @ts-ignore
151
- expect(axiosMockInstance.history.get?.[0]?.headers.Authorization).toBe(
152
- `Bearer ${access_token}`,
153
- );
140
+ expect(axiosMockInstance.history.get?.[0]?.headers.Authorization).toBe(`Bearer ${access_token}`);
154
141
  });
155
142
 
156
143
  it("should retry request with new token if failed with 401 after token refresh, and requests after that have new token", async () => {
@@ -177,12 +164,8 @@ describe("HTTP Client", () => {
177
164
  expect(response.status).toBe(200);
178
165
  expect(response.data).toBe("success");
179
166
  expect(axiosMockInstance.history.get?.length).toBe(3); // 3 Calls 2 for the first request (request and retry), 1 for the second request
180
- expect(axiosMockInstance.history.get?.[1]?.headers?.Authorization).toBe(
181
- "Bearer " + newAccessToken,
182
- );
183
- expect(axiosMockInstance.history.get?.[2]?.headers?.Authorization).toBe(
184
- "Bearer " + newAccessToken,
185
- );
167
+ expect(axiosMockInstance.history.get?.[1]?.headers?.Authorization).toBe(`Bearer ${newAccessToken}`);
168
+ expect(axiosMockInstance.history.get?.[2]?.headers?.Authorization).toBe(`Bearer ${newAccessToken}`);
186
169
  });
187
170
 
188
171
  it("should fail request if token refresh fails and publish an event", async () => {
@@ -192,9 +175,7 @@ describe("HTTP Client", () => {
192
175
  const brokerSpy = vi.spyOn(broker, "publish");
193
176
  axiosMockInstance.onGet("/api/clinical-course").replyOnce(401, "Unauthorized");
194
177
 
195
- await expect(axiosInstance.get("/api/clinical-course")).rejects.toThrow(
196
- "Request failed with status code 401",
197
- );
178
+ await expect(axiosInstance.get("/api/clinical-course")).rejects.toThrow("Request failed with status code 401");
198
179
 
199
180
  expect(axiosMockInstance.history.get?.length).toBe(1); // No retry happened
200
181
  expect(brokerSpy).toHaveBeenCalledWith(BROKER_EVENTS.shell.refreshTokenFailed, {});
@@ -10,6 +10,9 @@ export class ImportDataManagerModal extends LitElement implements ConfirmationCo
10
10
  setResult: (result: undefined) => void;
11
11
  setIsValid: (isValid: boolean) => void;
12
12
  confirm: () => void;
13
+ // Injectat pel servei d'interacció igual que la resta; faltava des que es va afegir a la
14
+ // interfície.
15
+ confirmResult: (result: undefined) => void;
13
16
  cancel: () => void;
14
17
 
15
18
  render() {
@@ -11,6 +11,8 @@ import {
11
11
  class MockInteractionService extends PrimariaInteractionService {
12
12
  private mockConfirmResult: { confirmed: boolean; result?: unknown } = { confirmed: true };
13
13
 
14
+ dispose(): void {}
15
+
14
16
  // Mètode per configurar el resultat del mock
15
17
  setMockConfirmResult(result: { confirmed: boolean; result?: unknown }) {
16
18
  this.mockConfirmResult = result;
@@ -17,6 +17,14 @@ const defaultOptions: ConfirmationOptions = {
17
17
  hideIcon: false,
18
18
  };
19
19
 
20
+ /**
21
+ * Un diàleg pot ser un component de React o un web component. Només el primer es pot renderitzar
22
+ * com a JSX, i cal un predicat de tipus perquè TypeScript ho estrenyi al punt d'ús: guardar el
23
+ * resultat en un booleà no li serveix.
24
+ */
25
+ const isWebComponentConstructor = (component: unknown): component is new () => HTMLElement =>
26
+ typeof component === "function" && component.prototype instanceof HTMLElement;
27
+
20
28
  export class PrimariaInteractionServiceImpl extends PrimariaInteractionService {
21
29
  private readonly openDialogCancellers = new Set<() => void>();
22
30
 
@@ -44,13 +52,13 @@ export class PrimariaInteractionServiceImpl extends PrimariaInteractionService {
44
52
  }
45
53
 
46
54
  const DialogWrapper = () => {
47
- const [result, setResult] = useState<ConfirmationResult<TResult | undefined>>();
55
+ const [result, setResult] = useState<TResult>();
48
56
  const [isValid, setIsValid] = useState(true);
49
- const dialogComponentRef = useRef(null);
57
+ const dialogComponentRef = useRef<HTMLDivElement>(null);
50
58
  const dialogRef = useRef<HTMLDivElement>(null);
51
59
  const modalRef = useRef<HTMLDivElement>(null);
52
60
 
53
- const isWebComponent = typeof DialogComponent === "function" && DialogComponent.prototype instanceof HTMLElement;
61
+ const isWebComponent = isWebComponentConstructor(DialogComponent);
54
62
 
55
63
  useEffect(() => {
56
64
  window.addEventListener("keydown", handleKeydown);
@@ -73,7 +81,7 @@ export class PrimariaInteractionServiceImpl extends PrimariaInteractionService {
73
81
  handleClose(false);
74
82
  }
75
83
  };
76
- const handleOutsideClick = (event: Event) => {
84
+ const handleOutsideClick = (event: React.MouseEvent<HTMLDivElement>) => {
77
85
  if (dialogRef.current) {
78
86
  if (event.target === modalRef.current && finalOptions.closeOnOutsideClick) {
79
87
  handleClose(false);
@@ -81,6 +89,9 @@ export class PrimariaInteractionServiceImpl extends PrimariaInteractionService {
81
89
  }
82
90
  };
83
91
 
92
+ // L'efecte només ha de crear el web component quan canvia el component o les dades.
93
+ // Afegir-hi els handlers i `isValid` el faria recrear a cada render, perdent l'estat.
94
+ // biome-ignore lint/correctness/useExhaustiveDependencies: recrear-lo perdria l'estat
84
95
  useEffect(() => {
85
96
  if (isWebComponent && dialogComponentRef.current) {
86
97
  const parent = dialogComponentRef.current;
@@ -113,7 +124,7 @@ export class PrimariaInteractionServiceImpl extends PrimariaInteractionService {
113
124
  resolvePromise(finalResult, true);
114
125
  };
115
126
 
116
- const resolvePromise = (finalResult: TResult, confirmed: boolean) => {
127
+ const resolvePromise = (finalResult: TResult | undefined, confirmed: boolean) => {
117
128
  if (isClosing) return;
118
129
 
119
130
  isClosing = true;
@@ -153,7 +164,7 @@ export class PrimariaInteractionServiceImpl extends PrimariaInteractionService {
153
164
  };
154
165
 
155
166
  const _renderContent = () => {
156
- return isWebComponent ? (
167
+ return isWebComponentConstructor(DialogComponent) ? (
157
168
  <div ref={dialogComponentRef} />
158
169
  ) : (
159
170
  <DialogComponent
@@ -181,7 +192,7 @@ export class PrimariaInteractionServiceImpl extends PrimariaInteractionService {
181
192
  fill
182
193
  class={`dss-dialog-header-state--${getHeadIconVariantClass()}`}
183
194
  icon={getHeadIcon()}
184
- ></dss-icon>
195
+ />
185
196
  )}
186
197
  {finalOptions.title}
187
198
  </div>
@@ -193,7 +204,7 @@ export class PrimariaInteractionServiceImpl extends PrimariaInteractionService {
193
204
  variant="neutral"
194
205
  icon="close"
195
206
  onClick={() => handleClose(false)}
196
- ></dss-icon-button>
207
+ />
197
208
  )}
198
209
  </div>
199
210
  )}
@@ -11,44 +11,48 @@ import { getLanguage, getLocales, localizerFactory, setLocales } from "@uxland/l
11
11
  * getCurrentLanguage: () => string,
12
12
  * }>} A promise that resolves to an object with methods for translating strings and getting translations.
13
13
  */
14
- export const createLocaleManager =
15
- (pluginId: string) => (translations: Record<string, Record<string, string>>) => {
16
- const localizer = localizerFactory(getLanguage(), translations, "", true);
17
- setLocales(translations);
18
- return Promise.resolve({
19
- /**
20
- * Translates the given path using the provided variables and the plugin's ID.
21
- *
22
- * @param {string} path - The path to be translated.
23
- * @param {Record<string, string>} [variables] - Optional variables to be used in the translation.
24
- * @return {string} The translated string, or the original path if an error occurs.
25
- */
26
- translate: (path: string, variables?: Record<string, string>) => {
27
- try {
28
- return localizer(`${pluginId}.${path}`, variables);
29
- } catch (e) {
30
- console.error(e);
31
- return path;
32
- }
33
- },
34
- /**
35
- * Retrieves the translations for the current language and plugin.
36
- *
37
- * @return {Record<string, string>} The translations for the current language and plugin.
38
- */
39
- getTranslations: () => {
40
- const translations = getLocales();
41
- const language = getLanguage();
42
- return translations[language][pluginId] || {};
43
- },
14
+ /** Arbre de traduccions: claus imbricades fins a arribar al text. */
15
+ export interface LocalizationMessages {
16
+ [key: string]: string | LocalizationMessages;
17
+ }
44
18
 
45
- /**
46
- * Retrieves the current language.
47
- *
48
- * @return {string} The current language.
49
- */
50
- getCurrentLanguage: () => {
51
- return getLanguage();
52
- },
53
- });
54
- };
19
+ export const createLocaleManager = (pluginId: string) => (translations: Record<string, LocalizationMessages>) => {
20
+ const localizer = localizerFactory(getLanguage(), translations, "", true);
21
+ setLocales(translations);
22
+ return Promise.resolve({
23
+ /**
24
+ * Translates the given path using the provided variables and the plugin's ID.
25
+ *
26
+ * @param {string} path - The path to be translated.
27
+ * @param {Record<string, string>} [variables] - Optional variables to be used in the translation.
28
+ * @return {string} The translated string, or the original path if an error occurs.
29
+ */
30
+ translate: (path: string, variables?: Record<string, string>) => {
31
+ try {
32
+ return localizer(`${pluginId}.${path}`, variables);
33
+ } catch (e) {
34
+ console.error(e);
35
+ return path;
36
+ }
37
+ },
38
+ /**
39
+ * Retrieves the translations for the current language and plugin.
40
+ *
41
+ * @return {Record<string, string>} The translations for the current language and plugin.
42
+ */
43
+ getTranslations: () => {
44
+ const translations = getLocales();
45
+ const language = getLanguage();
46
+ return translations[language][pluginId] || {};
47
+ },
48
+
49
+ /**
50
+ * Retrieves the current language.
51
+ *
52
+ * @return {string} The current language.
53
+ */
54
+ getCurrentLanguage: () => {
55
+ return getLanguage();
56
+ },
57
+ });
58
+ };
@@ -1,3 +1,4 @@
1
+ // @vitest-environment happy-dom
1
2
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
3
  import { PrimariaNotificationServiceImpl } from "./notification.service-impl";
3
4
 
@@ -15,7 +16,7 @@ describe("PrimariaNotificationServiceImpl", () => {
15
16
  vi.useRealTimers();
16
17
  });
17
18
 
18
- const assertToast = (expectedState: string, message: string, duration: number = 5000) => {
19
+ const assertToast = (expectedState: string, message: string, duration = 5000) => {
19
20
  const toast = document.querySelector("dss-toast") as HTMLElement;
20
21
  expect(toast).not.toBeNull();
21
22
  expect(toast.getAttribute("isshow")).toBe("true");
@@ -1,9 +1,10 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { createRegionManagerProxy } from "./region-manager";
1
+ // @vitest-environment happy-dom
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { BROKER_EVENTS } from "../broker/broker-events";
4
+ import { createRegionManagerProxy } from "./region-manager";
4
5
 
5
6
  const pluginId = "test-plugin";
6
- const mockView = { id: "view1" };
7
+ const mockView = { id: "view1", factory: () => Promise.resolve(document.createElement("div")) };
7
8
 
8
9
  const createMockRegion = () => ({
9
10
  activate: vi.fn(),
@@ -26,6 +27,7 @@ describe("RegionManagerProxy", () => {
26
27
  registerViewWithRegion: vi.fn(),
27
28
  unregisterViewFromRegion: vi.fn(),
28
29
  getRegion: vi.fn().mockReturnValue(regionMock),
30
+ getRegisteredViews: vi.fn().mockReturnValue([]),
29
31
  destroy: vi.fn(),
30
32
  };
31
33
 
@@ -127,4 +129,58 @@ describe("RegionManagerProxy", () => {
127
129
  proxy._destroy();
128
130
  expect(regionManager.destroy).toHaveBeenCalled();
129
131
  });
132
+
133
+ describe("_removeAllViews", () => {
134
+ /**
135
+ * Es crida en tancar, abans del `dispose` dels plugins, perque els cleanups dels `useEffect`
136
+ * trobin els handlers del broker encara registrats. Ha d'esperar cada desmuntatge: el
137
+ * `clear()`/`destroy()` del regionManager fan `forEach` amb crides asincrones sense
138
+ * esperar-les, i per aixo no serveixen.
139
+ */
140
+ it("desregistra totes les vistes de totes les regions que tenen", async () => {
141
+ regionManager.getRegisteredViews = vi.fn((regionName: string) =>
142
+ regionName === "main-region"
143
+ ? [
144
+ { key: "a::v1", view: mockView },
145
+ { key: "b::v2", view: mockView },
146
+ ]
147
+ : [],
148
+ );
149
+
150
+ await proxy._removeAllViews();
151
+
152
+ expect(regionManager.unregisterViewFromRegion).toHaveBeenCalledWith("main-region", "a::v1");
153
+ expect(regionManager.unregisterViewFromRegion).toHaveBeenCalledWith("main-region", "b::v2");
154
+ expect(regionManager.unregisterViewFromRegion).toHaveBeenCalledTimes(2);
155
+ });
156
+
157
+ it("espera cada desmuntatge abans de continuar amb el seguent", async () => {
158
+ const order: string[] = [];
159
+ regionManager.getRegisteredViews = vi.fn((regionName: string) =>
160
+ regionName === "main-region"
161
+ ? [
162
+ { key: "a::v1", view: mockView },
163
+ { key: "b::v2", view: mockView },
164
+ ]
165
+ : [],
166
+ );
167
+ regionManager.unregisterViewFromRegion = vi.fn(async (_region: string, key: string) => {
168
+ order.push(`start:${key}`);
169
+ await new Promise((resolve) => setTimeout(resolve, 0));
170
+ order.push(`done:${key}`);
171
+ });
172
+
173
+ await proxy._removeAllViews();
174
+
175
+ expect(order).toEqual(["start:a::v1", "done:a::v1", "start:b::v2", "done:b::v2"]);
176
+ });
177
+
178
+ it("no fa res si cap regio te vistes", async () => {
179
+ regionManager.getRegisteredViews = vi.fn().mockReturnValue([]);
180
+
181
+ await proxy._removeAllViews();
182
+
183
+ expect(regionManager.unregisterViewFromRegion).not.toHaveBeenCalled();
184
+ });
185
+ });
130
186
  });
@@ -1,9 +1,9 @@
1
- import { HarmonixRegionManager, IRegionManager, HarmonixViewDefinition } from "@uxland/harmonix";
2
- import { PluginInfo } from "../../handle-plugins";
1
+ import { HarmonixRegionManager, HarmonixViewDefinition, IRegionManager } from "@uxland/harmonix";
3
2
  import { IRegion } from "@uxland/regions";
4
- import { shellRegions, clinicalMonitoringRegions, activityHistoryRegions, administrativeManagementRegions } from "./regions";
5
- import { PrimariaBroker } from "../broker/primaria-broker";
3
+ import { PluginInfo } from "../../handle-plugins";
6
4
  import { BROKER_EVENTS } from "../broker/broker-events";
5
+ import { PrimariaBroker } from "../broker/primaria-broker";
6
+ import { activityHistoryRegions, administrativeManagementRegions, clinicalMonitoringRegions, shellRegions } from "./regions";
7
7
 
8
8
  export interface PrimariaRegionManager extends HarmonixRegionManager {
9
9
  regions: {
@@ -18,9 +18,18 @@ export interface PrimariaRegionManager extends HarmonixRegionManager {
18
18
  activateMainView(viewId: string): Promise<void>;
19
19
  activatePluginView(regionName: string, pluginId: string, viewId: string): Promise<void>;
20
20
  getCurrentMainViewActive(): string | undefined;
21
- _destroy(): void;
21
+ _removeAllViews(): Promise<void>;
22
+ _destroy(): Promise<void>;
22
23
  }
23
24
 
25
+ /** Totes les regions que coneix el shell, per poder-les recórrer al tancar. */
26
+ const ALL_REGION_NAMES: string[] = [
27
+ ...Object.values(shellRegions),
28
+ ...Object.values(clinicalMonitoringRegions),
29
+ ...Object.values(activityHistoryRegions),
30
+ ...Object.values(administrativeManagementRegions),
31
+ ];
32
+
24
33
  class RegionManagerProxy implements PrimariaRegionManager {
25
34
  regions = {
26
35
  shell: shellRegions,
@@ -109,14 +118,24 @@ class RegionManagerProxy implements PrimariaRegionManager {
109
118
  */
110
119
  isViewActive(regionName: string, viewId: string) {
111
120
  const region = this.regionManager.getRegion(regionName);
112
- const isViewActive = region.currentActiveViews.some((view) => view.id === viewId);
121
+ // `ViewDefinition` de @uxland/regions no declara `id`, però el shell només hi registra
122
+ // `HarmonixViewDefinition`, que sí que en porta.
123
+ const isViewActive = region.currentActiveViews.some((view) => (view as HarmonixViewDefinition).id === viewId);
113
124
  return Promise.resolve(isViewActive);
114
125
  }
115
126
  async registerQuickAction(view: HarmonixViewDefinition): Promise<void> {
116
- await this.regionManager.registerViewWithRegion(this.regions.shell.quickActions, `${this.pluginInfo.pluginId}::${view.id}`, view);
127
+ await this.regionManager.registerViewWithRegion(
128
+ this.regions.shell.quickActions,
129
+ `${this.pluginInfo.pluginId}::${view.id}`,
130
+ view,
131
+ );
117
132
  }
118
133
  async registerNavigationMenu(view: HarmonixViewDefinition): Promise<void> {
119
- await this.regionManager.registerViewWithRegion(this.regions.shell.navigationMenu, `${this.pluginInfo.pluginId}::${view.id}`, view);
134
+ await this.regionManager.registerViewWithRegion(
135
+ this.regions.shell.navigationMenu,
136
+ `${this.pluginInfo.pluginId}::${view.id}`,
137
+ view,
138
+ );
120
139
  }
121
140
  async registerMainView(view: HarmonixViewDefinition): Promise<void> {
122
141
  await this.regionManager.registerViewWithRegion(this.regions.shell.main, `${this.pluginInfo.pluginId}::${view.id}`, view);
@@ -173,8 +192,31 @@ class RegionManagerProxy implements PrimariaRegionManager {
173
192
  this.broker.publish(BROKER_EVENTS.shell.mainViewChanged, { viewId });
174
193
  }
175
194
 
176
- _destroy() {
177
- this.regionManager.destroy();
195
+ /**
196
+ * Desmunta totes les vistes de totes les regions, i espera que acabin.
197
+ *
198
+ * Es crida en tancar, ABANS de fer el `dispose` dels plugins: els cleanups dels `useEffect` que
199
+ * s'executen en desmuntar-se poden enviar comandes pel broker, i han de trobar el handler encara
200
+ * registrat. Cada plugin ja ho fa pel seu compte al `dispose`, però això només cobreix els
201
+ * plugins d'aquest repositori; els de tercers (econsulta, allergies…) no els controlem.
202
+ *
203
+ * Fa servir `unregisterViewFromRegion` i no `destroy()`/`clear()` del regionManager perquè
204
+ * aquests fan `forEach` amb crides asíncrones sense esperar-les, així que retornen abans que les
205
+ * vistes s'hagin desmuntat de debò.
206
+ *
207
+ * És idempotent: desregistrar una vista que ja no hi és no fa res.
208
+ */
209
+ async _removeAllViews(): Promise<void> {
210
+ for (const regionName of ALL_REGION_NAMES) {
211
+ const registered = this.regionManager.getRegisteredViews(regionName) ?? [];
212
+ for (const { key } of [...registered]) {
213
+ await this.regionManager.unregisterViewFromRegion(regionName, key);
214
+ }
215
+ }
216
+ }
217
+
218
+ async _destroy() {
219
+ await this.regionManager.destroy();
178
220
  }
179
221
  }
180
222