@uxland/primary-shell 8.0.0-rc.0 → 8.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/component-BQj4158z.js +68 -0
- package/dist/component-BQj4158z.js.map +1 -0
- package/dist/{index-8n9_rIYI.js → index-BwN0QusL.js} +19320 -19795
- package/dist/index-BwN0QusL.js.map +1 -0
- package/dist/index.js +28 -17
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +2667 -2694
- package/dist/index.umd.cjs.map +1 -1
- package/dist/{plugin-B5E6LZ4X.js → plugin-BE23BSP6.js} +5817 -5685
- package/dist/plugin-BE23BSP6.js.map +1 -0
- package/dist/primary/shell/src/UI/components/clinical-pathways-action-menu/clinical-pathways-action-menu.d.ts +4 -12
- package/dist/primary/shell/src/UI/components/communication-action-menu/communication-action-menu.d.ts +4 -12
- package/dist/primary/shell/src/UI/components/petitioner-action-menu/petitioner-action-menu.d.ts +4 -12
- package/dist/primary/shell/src/UI/components/sidenav-action-menu/sidenav-action-menu.d.ts +27 -0
- package/dist/primary/shell/src/UI/components/sidenav-action-menu/template.d.ts +2 -0
- package/dist/primary/shell/src/UI/shared-components/primaria-text-editor/primaria-rich-text-editor.d.ts +7 -1
- package/dist/primary/shell/src/api/broker/broker-events.d.ts +15 -0
- package/dist/primary/shell/src/api/broker/broker-requests.d.ts +20 -0
- package/dist/primary/shell/src/api/broker/primaria-broker.d.ts +4 -0
- package/dist/primary/shell/src/api/ecap-event-manager/ecap-event-manager.d.ts +2 -2
- package/dist/primary/shell/src/api/import-data-manager/component/component.d.ts +1 -0
- package/dist/primary/shell/src/api/region-manager/region-manager.d.ts +10 -3
- package/dist/primary/shell/src/api/region-manager/remove-view-idempotency.test.d.ts +1 -0
- package/dist/primary/shell/src/disposer.d.ts +1 -1
- package/dist/primary/shell/src/features/bootstrapper.d.ts +1 -1
- package/dist/primary/shell/src/features/exit/handler.d.ts +12 -0
- package/dist/primary/shell/src/features/get-user-info/backend-adapter.d.ts +4 -2
- package/dist/primary/shell/src/features/get-user-info/pds-token.d.ts +26 -0
- package/dist/primary/shell/src/features/get-visi-id/action.d.ts +2 -0
- package/dist/primary/shell/src/index.d.ts +5 -0
- package/dist/primary/shell/src/locales.d.ts +1 -0
- package/package.json +5 -8
- package/src/UI/components/clinical-pathways-action-menu/clinical-pathways-action-menu.ts +6 -41
- package/src/UI/components/communication-action-menu/communication-action-menu.ts +6 -41
- package/src/UI/components/petitioner-action-menu/petitioner-action-menu.ts +6 -41
- package/src/UI/components/primaria-shell/primaria-shell.ts +3 -1
- package/src/UI/components/sidenav-action-menu/sidenav-action-menu.ts +62 -0
- package/src/UI/components/{petitioner-action-menu → sidenav-action-menu}/template.ts +3 -3
- package/src/UI/internal-views/doctor-nav-menu.ts +9 -2
- package/src/UI/shared-components/bootstrapper.ts +3 -1
- package/src/UI/shared-components/primaria-nav-item/primaria-nav-item.ts +3 -1
- package/src/UI/shared-components/primaria-text-editor/primaria-rich-text-editor.ts +34 -21
- package/src/api/activity-monitor/activity-monitor.test.ts +1 -1
- package/src/api/broker/broker-events.ts +9 -0
- package/src/api/broker/broker-requests.ts +20 -0
- package/src/api/broker/factory.ts +2 -0
- package/src/api/broker/primaria-broker.ts +4 -0
- package/src/api/ecap-event-manager/ecap-event-manager.test.ts +12 -4
- package/src/api/ecap-event-manager/ecap-event-manager.ts +5 -1
- package/src/api/http-client/http-client.test.ts +12 -31
- package/src/api/import-data-manager/component/component.ts +3 -0
- package/src/api/import-data-manager/component/styles.css +25 -32
- package/src/api/import-data-manager/component/template.ts +16 -0
- package/src/api/import-data-manager/import-data-manager-impl.test.ts +2 -0
- package/src/api/interaction-service/interaction-service-impl.tsx +19 -8
- package/src/api/localization/localization.ts +44 -40
- package/src/api/notification-service/notification-service-impl.test.ts +2 -1
- package/src/api/region-manager/region-manager.test.ts +112 -3
- package/src/api/region-manager/region-manager.ts +85 -10
- package/src/api/region-manager/remove-view-idempotency.test.ts +98 -0
- package/src/api/token-manager/token-manager.test.ts +3 -6
- package/src/disposer.ts +2 -2
- package/src/features/bootstrapper.ts +2 -2
- package/src/features/exit/handler.test.ts +60 -0
- package/src/features/exit/handler.ts +19 -3
- package/src/features/get-user-info/backend-adapter.ts +30 -18
- package/src/features/get-user-info/handler.test.ts +8 -2
- package/src/features/get-user-info/pds-token.ts +28 -0
- package/src/features/get-visi-id/action.ts +11 -0
- package/src/index.ts +40 -1
- package/src/locales.ts +1 -0
- package/src/types/custom.d.ts +30 -0
- package/dist/component-Bt_gBs5e.js +0 -52
- package/dist/component-Bt_gBs5e.js.map +0 -1
- package/dist/index-8n9_rIYI.js.map +0 -1
- package/dist/plugin-B5E6LZ4X.js.map +0 -1
- package/dist/primary/shell/src/UI/components/clinical-pathways-action-menu/template.d.ts +0 -2
- package/dist/primary/shell/src/UI/components/communication-action-menu/template.d.ts +0 -2
- package/dist/primary/shell/src/UI/components/petitioner-action-menu/template.d.ts +0 -2
- package/src/UI/components/clinical-pathways-action-menu/template.ts +0 -24
- package/src/UI/components/communication-action-menu/styles.css +0 -29
- package/src/UI/components/communication-action-menu/template.ts +0 -24
- package/src/UI/components/petitioner-action-menu/styles.css +0 -29
- /package/src/UI/components/{clinical-pathways-action-menu → sidenav-action-menu}/styles.css +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { shellApi } from "../../api/api";
|
|
2
2
|
import { getPatientCip } from "../../features/get-patient-cip/action";
|
|
3
|
+
import { getVisiId } from "../../features/get-visi-id/action";
|
|
3
4
|
import { navigateToEcap } from "../../features/navigate-to-ecap/navigate-to-ecap";
|
|
4
5
|
import { ClinicalPathwaysActionMenu } from "../components/clinical-pathways-action-menu/clinical-pathways-action-menu";
|
|
5
6
|
import { PetitionerActionMenu } from "../components/petitioner-action-menu/petitioner-action-menu";
|
|
@@ -69,7 +70,10 @@ const doctorNavMenuItems: MenuItemConfig[] = [
|
|
|
69
70
|
{
|
|
70
71
|
icon: "open_in_new",
|
|
71
72
|
label: "Prescripció",
|
|
72
|
-
callbackFn: () =>
|
|
73
|
+
callbackFn: async () => {
|
|
74
|
+
const visiId = await getVisiId(shellApi);
|
|
75
|
+
navigateToEcap("PRESCRIPCIO_ACTIVA", "NO_TANCAR", visiId ? { VisiId: visiId } : {});
|
|
76
|
+
},
|
|
73
77
|
},
|
|
74
78
|
{
|
|
75
79
|
icon: "open_in_new",
|
|
@@ -102,7 +106,10 @@ const doctorNavMenuItems: MenuItemConfig[] = [
|
|
|
102
106
|
label: "Incapacitat temporal",
|
|
103
107
|
type: "item",
|
|
104
108
|
sortHint: "0100",
|
|
105
|
-
callbackFn: () =>
|
|
109
|
+
callbackFn: async () => {
|
|
110
|
+
const visiId = await getVisiId(shellApi);
|
|
111
|
+
navigateToEcap("REV_IT", "NO_TANCAR", visiId ? { VisiId: visiId } : {});
|
|
112
|
+
},
|
|
106
113
|
},
|
|
107
114
|
{
|
|
108
115
|
id: "visits",
|
|
@@ -12,5 +12,7 @@ export const useSharedUI = () => {
|
|
|
12
12
|
customElement("primaria-rich-text-editor")(PrimariaRichTextEditor);
|
|
13
13
|
customElement("primaria-nav-tree-menu")(PrimariaNavTreeMenu);
|
|
14
14
|
customElement("primaria-character-limit-badge")(PrimariaCharacterLimitBadge);
|
|
15
|
-
|
|
15
|
+
// @ts-ignore: `PrimariaRegion` estén el mixin `PrimariaRegionHost`, que està tipat com a
|
|
16
|
+
// `any`, així que TypeScript no el reconeix com a `CustomElementClass`.
|
|
17
|
+
customElement("primaria-region")(PrimariaRegion);
|
|
16
18
|
};
|
|
@@ -48,7 +48,9 @@ export class PrimariaNavItem extends regionView(LitElement) {
|
|
|
48
48
|
|
|
49
49
|
_subscribeEvents() {
|
|
50
50
|
const subscription = shellApi.broker.subscribe(BROKER_EVENTS.shell.mainViewChanged, (payload: { viewId: string }) => {
|
|
51
|
-
|
|
51
|
+
// `ViewDefinition` de @uxland/regions no declara `id`, però el shell només hi registra
|
|
52
|
+
// vistes que en porten.
|
|
53
|
+
this.isActive = payload.viewId === (this.view as { id?: string } | undefined)?.id;
|
|
52
54
|
});
|
|
53
55
|
this.subscriptions.push(subscription);
|
|
54
56
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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,8 +1,17 @@
|
|
|
1
|
+
import { PLUGIN_BROKER_EVENTS } from "@primaria/plugins-core";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Diccionari centralitzat d'events del broker.
|
|
3
5
|
* Events agrupats per plugin/iniciativa per facilitar l'organització i escalabilitat.
|
|
6
|
+
*
|
|
7
|
+
* Els grups que hi ha aquí escrits són els que el shell publica o consumeix. Els
|
|
8
|
+
* contractes entre iniciatives, en què el shell no és part, es declaren a
|
|
9
|
+
* `@primaria/plugins-core` i s'hi escampen a sota: el shell només fa de canal de
|
|
10
|
+
* distribució, perquè és l'únic paquet que instal·len les iniciatives externes.
|
|
11
|
+
* Els que porten resposta no són aquí, sinó a `BROKER_REQUESTS`.
|
|
4
12
|
*/
|
|
5
13
|
export const BROKER_EVENTS = {
|
|
14
|
+
...PLUGIN_BROKER_EVENTS,
|
|
6
15
|
activityHistory: {
|
|
7
16
|
refreshDataRequested: "refresh_history_data_requested",
|
|
8
17
|
errorLoadingData: "error_loading_activity_history_data",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { PLUGIN_BROKER_REQUESTS } from "@primaria/plugins-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Diccionari centralitzat de requests del broker: els missatges que porten
|
|
5
|
+
* resposta i es criden amb `send` / `registerRequest`.
|
|
6
|
+
*
|
|
7
|
+
* Separat de `BROKER_EVENTS` a propòsit. Un event (`publish`/`subscribe`) i una
|
|
8
|
+
* request (`send`/`registerRequest`) són mecanismes diferents, i barrejar-los
|
|
9
|
+
* en una constant que es diu "events" deixa qui l'autocompleta sense saber quin
|
|
10
|
+
* dels dos ha de cridar.
|
|
11
|
+
*
|
|
12
|
+
* Ara mateix només conté contractes entre iniciatives, declarats a
|
|
13
|
+
* `@primaria/plugins-core`. Si algun dia el shell exposa requests pròpies,
|
|
14
|
+
* s'escriuen aquí al costat, igual que a `BROKER_EVENTS`.
|
|
15
|
+
*/
|
|
16
|
+
export const BROKER_REQUESTS = {
|
|
17
|
+
...PLUGIN_BROKER_REQUESTS,
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
export type BrokerRequests = typeof BROKER_REQUESTS;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { IEvent, IEventClass, IRequest, IRequestClass } from "@uxland/harmonix";
|
|
2
2
|
import { BROKER_EVENTS } from "./broker-events";
|
|
3
|
+
import { BROKER_REQUESTS } from "./broker-requests";
|
|
3
4
|
import type { BrokerDisposableHandler, PrimariaBroker } from "./primaria-broker";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -30,6 +31,7 @@ type Handler = (message: any) => unknown;
|
|
|
30
31
|
|
|
31
32
|
class Broker implements PrimariaBroker {
|
|
32
33
|
public readonly events = BROKER_EVENTS;
|
|
34
|
+
public readonly requests = BROKER_REQUESTS;
|
|
33
35
|
|
|
34
36
|
/** One handler per request type (request/response). */
|
|
35
37
|
private readonly requestHandlers = new Map<MessageKey, Handler>();
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { HarmonixBroker } from "@uxland/harmonix";
|
|
2
2
|
import { type BrokerEvents } from "./broker-events";
|
|
3
|
+
import { type BrokerRequests } from "./broker-requests";
|
|
3
4
|
|
|
4
5
|
export interface PrimariaBroker extends HarmonixBroker {
|
|
6
|
+
/** Noms dels events: `publish` / `subscribe`. */
|
|
5
7
|
events: BrokerEvents;
|
|
8
|
+
/** Noms de les requests: `send` / `registerRequest`. */
|
|
9
|
+
requests: BrokerRequests;
|
|
6
10
|
/** Internal: removes every registration (shell teardown only). */
|
|
7
11
|
_clear(): void;
|
|
8
12
|
}
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
84
|
+
const postedEvent = JSON.parse(firstPostedMessage(mockPostMessage));
|
|
77
85
|
expect(postedEvent.DIAGNOSTICS).toEqual([]);
|
|
78
86
|
});
|
|
79
87
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { LAST_WORKED_DIAGNOSTICS_KEY } from "@primaria/plugins-core";
|
|
1
2
|
import type { PrimariaGlobalStateManager } from "../global-state/global-state";
|
|
2
3
|
import { IEcapEvent } from "./typings";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
// La clau la declara `@primaria/plugins-core`: qui l'escriu és clinical-course i
|
|
6
|
+
// qui la llegeix és aquest mànager, així que no pot viure a cap dels dos costats.
|
|
7
|
+
// Es re-exporta perquè els imports que ja hi havia segueixin funcionant.
|
|
8
|
+
export { LAST_WORKED_DIAGNOSTICS_KEY };
|
|
5
9
|
|
|
6
10
|
const EVENTS_WITH_DIAGNOSTICS = [
|
|
7
11
|
"PROVES_ORDRES_CLINIQUES_CONSULTAR",
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() {
|
|
@@ -9,28 +9,31 @@
|
|
|
9
9
|
overflow: hidden;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
/*
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
/* Three columns so the title is centred on the modal, not on the space left
|
|
13
|
+
over by the close button. */
|
|
14
|
+
.header {
|
|
15
|
+
display: grid;
|
|
16
|
+
grid-template-columns: 1fr auto 1fr;
|
|
15
17
|
align-items: center;
|
|
16
|
-
justify-content: space-between;
|
|
17
|
-
text-align: center;
|
|
18
18
|
color: black;
|
|
19
19
|
padding: var(--dss-spacing-sm);
|
|
20
|
+
}
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
.header__title {
|
|
23
|
+
grid-column: 2;
|
|
24
|
+
text-align: center;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.header__close {
|
|
28
|
+
grid-column: 3;
|
|
29
|
+
justify-self: end;
|
|
30
|
+
}
|
|
27
31
|
|
|
28
32
|
.content{
|
|
29
33
|
display: grid;
|
|
30
34
|
flex: 1;
|
|
31
35
|
min-height: 0;
|
|
32
|
-
grid-template-columns:
|
|
33
|
-
gap: 16px;
|
|
36
|
+
grid-template-columns: 50% 50%;
|
|
34
37
|
padding: 16px;
|
|
35
38
|
}
|
|
36
39
|
|
|
@@ -43,42 +46,32 @@
|
|
|
43
46
|
.content primaria-region > div {
|
|
44
47
|
width: 100%;
|
|
45
48
|
height: 100%;
|
|
46
|
-
border: 1px solid var(--color-neutral-200);
|
|
47
49
|
border-radius: 8px;
|
|
48
|
-
padding: 16px;
|
|
50
|
+
padding: 0 16px;
|
|
49
51
|
box-sizing: border-box;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
/*
|
|
53
|
-
.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
/* Left column: vertical stack of multiple views with a gap. The last view
|
|
58
|
-
takes the remaining space and handles its own scroll; preceding views
|
|
59
|
-
keep their natural height. */
|
|
60
|
-
.content > primaria-region[name="import-data-activity-history-region"] > div {
|
|
54
|
+
/* Both columns are a vertical stack of injected views, 16px apart, that scrolls
|
|
55
|
+
as a whole. Each view keeps its natural height and bounds its own inner
|
|
56
|
+
scroll, and must not shrink — otherwise the column squeezes the views instead
|
|
57
|
+
of scrolling. */
|
|
58
|
+
.content > primaria-region > div {
|
|
61
59
|
display: flex;
|
|
62
60
|
flex-direction: column;
|
|
63
61
|
gap: 16px;
|
|
64
|
-
overflow:
|
|
62
|
+
overflow-y: auto;
|
|
65
63
|
}
|
|
66
64
|
|
|
67
|
-
.content > primaria-region
|
|
65
|
+
.content > primaria-region > div > * {
|
|
68
66
|
flex-shrink: 0;
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
.content > primaria-region[name="import-data-activity-history-region"] > div > *:last-child {
|
|
72
|
-
flex: 1;
|
|
73
|
-
min-height: 0;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
69
|
.footer {
|
|
77
70
|
display: flex;
|
|
78
71
|
flex-direction: row;
|
|
79
72
|
align-items: center;
|
|
80
73
|
background: white;
|
|
81
|
-
justify-content:
|
|
74
|
+
justify-content: center;
|
|
82
75
|
gap: 12px;
|
|
83
76
|
height: 56px;
|
|
84
77
|
border-top: 1px solid var(--color-neutral-100);
|
|
@@ -6,6 +6,21 @@ import { ImportDataManagerModal } from "./component";
|
|
|
6
6
|
export const template = (props: ImportDataManagerModal) => {
|
|
7
7
|
return html`
|
|
8
8
|
<div class="container">
|
|
9
|
+
<div class="header">
|
|
10
|
+
<dss-typography class="header__title" tag="h2" variant="headline-4">
|
|
11
|
+
${translate("importDataManager.title")}
|
|
12
|
+
</dss-typography>
|
|
13
|
+
<dss-icon-button
|
|
14
|
+
class="header__close"
|
|
15
|
+
icon="close"
|
|
16
|
+
size="lg"
|
|
17
|
+
variant="neutral"
|
|
18
|
+
label="${translate("importDataManager.actions.close")}"
|
|
19
|
+
@onClick="${() => {
|
|
20
|
+
props.cancel();
|
|
21
|
+
}}"
|
|
22
|
+
></dss-icon-button>
|
|
23
|
+
</div>
|
|
9
24
|
<div class="content">
|
|
10
25
|
<primaria-region name="${shellApi.regionManager.regions.shell.importDataActivityHistory}"></primaria-region>
|
|
11
26
|
<primaria-region name="${shellApi.regionManager.regions.shell.importData}"></primaria-region>
|
|
@@ -20,6 +35,7 @@ export const template = (props: ImportDataManagerModal) => {
|
|
|
20
35
|
variant="secondary"
|
|
21
36
|
></dss-button>
|
|
22
37
|
<dss-button
|
|
38
|
+
icon="add_circle"
|
|
23
39
|
label="${translate("importDataManager.actions.import")}"
|
|
24
40
|
@click="${() => {
|
|
25
41
|
props._accept();
|
|
@@ -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<
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
207
|
+
/>
|
|
197
208
|
)}
|
|
198
209
|
</div>
|
|
199
210
|
)}
|