@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
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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
|
-
|
|
2
|
-
import {
|
|
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
|
|
|
@@ -123,8 +125,115 @@ describe("RegionManagerProxy", () => {
|
|
|
123
125
|
expect(result).toBe("view1");
|
|
124
126
|
});
|
|
125
127
|
|
|
128
|
+
describe("getRegisteredMainViews", () => {
|
|
129
|
+
// `id: undefined` vol dir una definicio que no en declara, no una amb l'id a undefined.
|
|
130
|
+
const registeredView = (key: string, id?: string) => ({
|
|
131
|
+
key,
|
|
132
|
+
view: id === undefined ? { factory: mockView.factory } : { ...mockView, id },
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("desglossa la clau del registry en pluginId i viewId", () => {
|
|
136
|
+
regionManager.getRegisteredViews.mockReturnValue([
|
|
137
|
+
registeredView("appointments::main", "main"),
|
|
138
|
+
registeredView("activity-history::timeline", "timeline"),
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
expect(proxy.getRegisteredMainViews()).toEqual([
|
|
142
|
+
{ pluginId: "appointments", viewId: "main" },
|
|
143
|
+
{ pluginId: "activity-history", viewId: "timeline" },
|
|
144
|
+
]);
|
|
145
|
+
expect(regionManager.getRegisteredViews).toHaveBeenCalledWith(proxy.regions.shell.main);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* El registry s'omple en registrar la vista; `getRegion(main).currentViews` només té
|
|
150
|
+
* les vistes afegides a un host ja muntat, i `getRegion` retorna `undefined` mentre la
|
|
151
|
+
* regio no hi es. Per aixo el metode no ha de dependre de la regio.
|
|
152
|
+
*/
|
|
153
|
+
it("no depen que la regio main estigui muntada", () => {
|
|
154
|
+
regionManager.getRegion.mockReturnValue(undefined);
|
|
155
|
+
regionManager.getRegisteredViews.mockReturnValue([registeredView("appointments::main", "main")]);
|
|
156
|
+
|
|
157
|
+
expect(proxy.getRegisteredMainViews()).toEqual([{ pluginId: "appointments", viewId: "main" }]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("retorna una llista buida quan no hi ha res registrat", () => {
|
|
161
|
+
regionManager.getRegisteredViews.mockReturnValue([]);
|
|
162
|
+
|
|
163
|
+
expect(proxy.getRegisteredMainViews()).toEqual([]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Les vistes dels plugins de tercers (econsulta, allergies...) es poden registrar
|
|
167
|
+
// directament contra el regionManager, sense el prefix que hi posa aquest proxy.
|
|
168
|
+
it("deixa el pluginId buit si la clau no porta prefix de plugin", () => {
|
|
169
|
+
regionManager.getRegisteredViews.mockReturnValue([registeredView("legacy-view", "legacy-view")]);
|
|
170
|
+
|
|
171
|
+
expect(proxy.getRegisteredMainViews()).toEqual([{ pluginId: "", viewId: "legacy-view" }]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("cau a la clau quan la definicio no declara id", () => {
|
|
175
|
+
regionManager.getRegisteredViews.mockReturnValue([registeredView("appointments::main", undefined)]);
|
|
176
|
+
|
|
177
|
+
expect(proxy.getRegisteredMainViews()).toEqual([{ pluginId: "appointments", viewId: "main" }]);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
126
181
|
it("_destroy should call regionManager.destroy", () => {
|
|
127
182
|
proxy._destroy();
|
|
128
183
|
expect(regionManager.destroy).toHaveBeenCalled();
|
|
129
184
|
});
|
|
185
|
+
|
|
186
|
+
describe("_removeAllViews", () => {
|
|
187
|
+
/**
|
|
188
|
+
* Es crida en tancar, abans del `dispose` dels plugins, perque els cleanups dels `useEffect`
|
|
189
|
+
* trobin els handlers del broker encara registrats. Ha d'esperar cada desmuntatge: el
|
|
190
|
+
* `clear()`/`destroy()` del regionManager fan `forEach` amb crides asincrones sense
|
|
191
|
+
* esperar-les, i per aixo no serveixen.
|
|
192
|
+
*/
|
|
193
|
+
it("desregistra totes les vistes de totes les regions que tenen", async () => {
|
|
194
|
+
regionManager.getRegisteredViews = vi.fn((regionName: string) =>
|
|
195
|
+
regionName === "main-region"
|
|
196
|
+
? [
|
|
197
|
+
{ key: "a::v1", view: mockView },
|
|
198
|
+
{ key: "b::v2", view: mockView },
|
|
199
|
+
]
|
|
200
|
+
: [],
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
await proxy._removeAllViews();
|
|
204
|
+
|
|
205
|
+
expect(regionManager.unregisterViewFromRegion).toHaveBeenCalledWith("main-region", "a::v1");
|
|
206
|
+
expect(regionManager.unregisterViewFromRegion).toHaveBeenCalledWith("main-region", "b::v2");
|
|
207
|
+
expect(regionManager.unregisterViewFromRegion).toHaveBeenCalledTimes(2);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("espera cada desmuntatge abans de continuar amb el seguent", async () => {
|
|
211
|
+
const order: string[] = [];
|
|
212
|
+
regionManager.getRegisteredViews = vi.fn((regionName: string) =>
|
|
213
|
+
regionName === "main-region"
|
|
214
|
+
? [
|
|
215
|
+
{ key: "a::v1", view: mockView },
|
|
216
|
+
{ key: "b::v2", view: mockView },
|
|
217
|
+
]
|
|
218
|
+
: [],
|
|
219
|
+
);
|
|
220
|
+
regionManager.unregisterViewFromRegion = vi.fn(async (_region: string, key: string) => {
|
|
221
|
+
order.push(`start:${key}`);
|
|
222
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
223
|
+
order.push(`done:${key}`);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
await proxy._removeAllViews();
|
|
227
|
+
|
|
228
|
+
expect(order).toEqual(["start:a::v1", "done:a::v1", "start:b::v2", "done:b::v2"]);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("no fa res si cap regio te vistes", async () => {
|
|
232
|
+
regionManager.getRegisteredViews = vi.fn().mockReturnValue([]);
|
|
233
|
+
|
|
234
|
+
await proxy._removeAllViews();
|
|
235
|
+
|
|
236
|
+
expect(regionManager.unregisterViewFromRegion).not.toHaveBeenCalled();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
130
239
|
});
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { HarmonixRegionManager,
|
|
2
|
-
import { PluginInfo } from "../../handle-plugins";
|
|
1
|
+
import { HarmonixRegionManager, HarmonixViewDefinition, IRegionManager } from "@uxland/harmonix";
|
|
3
2
|
import { IRegion } from "@uxland/regions";
|
|
4
|
-
import {
|
|
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
|
+
|
|
8
|
+
/** Una vista de la region main, amb el plugin que l'ha registrada. */
|
|
9
|
+
export interface RegisteredMainView {
|
|
10
|
+
pluginId: string;
|
|
11
|
+
viewId: string;
|
|
12
|
+
}
|
|
7
13
|
|
|
8
14
|
export interface PrimariaRegionManager extends HarmonixRegionManager {
|
|
9
15
|
regions: {
|
|
@@ -18,9 +24,19 @@ export interface PrimariaRegionManager extends HarmonixRegionManager {
|
|
|
18
24
|
activateMainView(viewId: string): Promise<void>;
|
|
19
25
|
activatePluginView(regionName: string, pluginId: string, viewId: string): Promise<void>;
|
|
20
26
|
getCurrentMainViewActive(): string | undefined;
|
|
21
|
-
|
|
27
|
+
getRegisteredMainViews(): RegisteredMainView[];
|
|
28
|
+
_removeAllViews(): Promise<void>;
|
|
29
|
+
_destroy(): Promise<void>;
|
|
22
30
|
}
|
|
23
31
|
|
|
32
|
+
/** Totes les regions que coneix el shell, per poder-les recórrer al tancar. */
|
|
33
|
+
const ALL_REGION_NAMES: string[] = [
|
|
34
|
+
...Object.values(shellRegions),
|
|
35
|
+
...Object.values(clinicalMonitoringRegions),
|
|
36
|
+
...Object.values(activityHistoryRegions),
|
|
37
|
+
...Object.values(administrativeManagementRegions),
|
|
38
|
+
];
|
|
39
|
+
|
|
24
40
|
class RegionManagerProxy implements PrimariaRegionManager {
|
|
25
41
|
regions = {
|
|
26
42
|
shell: shellRegions,
|
|
@@ -109,14 +125,24 @@ class RegionManagerProxy implements PrimariaRegionManager {
|
|
|
109
125
|
*/
|
|
110
126
|
isViewActive(regionName: string, viewId: string) {
|
|
111
127
|
const region = this.regionManager.getRegion(regionName);
|
|
112
|
-
|
|
128
|
+
// `ViewDefinition` de @uxland/regions no declara `id`, però el shell només hi registra
|
|
129
|
+
// `HarmonixViewDefinition`, que sí que en porta.
|
|
130
|
+
const isViewActive = region.currentActiveViews.some((view) => (view as HarmonixViewDefinition).id === viewId);
|
|
113
131
|
return Promise.resolve(isViewActive);
|
|
114
132
|
}
|
|
115
133
|
async registerQuickAction(view: HarmonixViewDefinition): Promise<void> {
|
|
116
|
-
await this.regionManager.registerViewWithRegion(
|
|
134
|
+
await this.regionManager.registerViewWithRegion(
|
|
135
|
+
this.regions.shell.quickActions,
|
|
136
|
+
`${this.pluginInfo.pluginId}::${view.id}`,
|
|
137
|
+
view,
|
|
138
|
+
);
|
|
117
139
|
}
|
|
118
140
|
async registerNavigationMenu(view: HarmonixViewDefinition): Promise<void> {
|
|
119
|
-
await this.regionManager.registerViewWithRegion(
|
|
141
|
+
await this.regionManager.registerViewWithRegion(
|
|
142
|
+
this.regions.shell.navigationMenu,
|
|
143
|
+
`${this.pluginInfo.pluginId}::${view.id}`,
|
|
144
|
+
view,
|
|
145
|
+
);
|
|
120
146
|
}
|
|
121
147
|
async registerMainView(view: HarmonixViewDefinition): Promise<void> {
|
|
122
148
|
await this.regionManager.registerViewWithRegion(this.regions.shell.main, `${this.pluginInfo.pluginId}::${view.id}`, view);
|
|
@@ -149,6 +175,32 @@ class RegionManagerProxy implements PrimariaRegionManager {
|
|
|
149
175
|
return mainView?.id;
|
|
150
176
|
}
|
|
151
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Totes les vistes registrades a la region main, amb el plugin que les ha registrat.
|
|
180
|
+
*
|
|
181
|
+
* Va pel registry del regionManager i no per `getRegion(main).currentViews`, i és a
|
|
182
|
+
* propòsit: el registry s'omple en registrar la vista, mentre que `currentViews` només
|
|
183
|
+
* té les que s'han afegit a un host de regió ja muntat —i `getRegion` retorna
|
|
184
|
+
* `undefined` si la regió encara no hi és—, així que cridar-ho des de l'`initialize()`
|
|
185
|
+
* d'un plugin donaria una llista incompleta o petaria.
|
|
186
|
+
*
|
|
187
|
+
* Les claus del registry són `pluginId::viewId`. El `viewId` es llegeix de la
|
|
188
|
+
* definició, que és qui en manda; de la clau només se'n treu el prefix del plugin, que
|
|
189
|
+
* pot no ser-hi si la vista l'ha registrada un plugin de tercers directament contra el
|
|
190
|
+
* regionManager, sense passar per aquest proxy.
|
|
191
|
+
*/
|
|
192
|
+
getRegisteredMainViews(): RegisteredMainView[] {
|
|
193
|
+
return this.regionManager.getRegisteredViews(this.regions.shell.main).map(({ key, view }) => {
|
|
194
|
+
const separatorIndex = key.indexOf("::");
|
|
195
|
+
const hasPluginPrefix = separatorIndex !== -1;
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
pluginId: hasPluginPrefix ? key.slice(0, separatorIndex) : "",
|
|
199
|
+
viewId: (view as HarmonixViewDefinition).id ?? (hasPluginPrefix ? key.slice(separatorIndex + 2) : key),
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
152
204
|
/**
|
|
153
205
|
* Activates a view if it is marked as default and the region currently has no active views.
|
|
154
206
|
*
|
|
@@ -173,8 +225,31 @@ class RegionManagerProxy implements PrimariaRegionManager {
|
|
|
173
225
|
this.broker.publish(BROKER_EVENTS.shell.mainViewChanged, { viewId });
|
|
174
226
|
}
|
|
175
227
|
|
|
176
|
-
|
|
177
|
-
|
|
228
|
+
/**
|
|
229
|
+
* Desmunta totes les vistes de totes les regions, i espera que acabin.
|
|
230
|
+
*
|
|
231
|
+
* Es crida en tancar, ABANS de fer el `dispose` dels plugins: els cleanups dels `useEffect` que
|
|
232
|
+
* s'executen en desmuntar-se poden enviar comandes pel broker, i han de trobar el handler encara
|
|
233
|
+
* registrat. Cada plugin ja ho fa pel seu compte al `dispose`, però això només cobreix els
|
|
234
|
+
* plugins d'aquest repositori; els de tercers (econsulta, allergies…) no els controlem.
|
|
235
|
+
*
|
|
236
|
+
* Fa servir `unregisterViewFromRegion` i no `destroy()`/`clear()` del regionManager perquè
|
|
237
|
+
* aquests fan `forEach` amb crides asíncrones sense esperar-les, així que retornen abans que les
|
|
238
|
+
* vistes s'hagin desmuntat de debò.
|
|
239
|
+
*
|
|
240
|
+
* És idempotent: desregistrar una vista que ja no hi és no fa res.
|
|
241
|
+
*/
|
|
242
|
+
async _removeAllViews(): Promise<void> {
|
|
243
|
+
for (const regionName of ALL_REGION_NAMES) {
|
|
244
|
+
const registered = this.regionManager.getRegisteredViews(regionName) ?? [];
|
|
245
|
+
for (const { key } of [...registered]) {
|
|
246
|
+
await this.regionManager.unregisterViewFromRegion(regionName, key);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async _destroy() {
|
|
252
|
+
await this.regionManager.destroy();
|
|
178
253
|
}
|
|
179
254
|
}
|
|
180
255
|
|
|
@@ -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
|
-
|
|
21
|
-
this.timeout(10000), // Si passen
|
|
20
|
+
this.unmountViewsAndDisposePlugins(), // S'intenta executar un dispose normal
|
|
21
|
+
this.timeout(10000), // Si passen 10s, 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
|
}
|