@uxland/primary-shell 4.2.0 → 4.3.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 (76) hide show
  1. package/dist/index.js +19452 -19117
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.umd.cjs +1681 -1549
  4. package/dist/index.umd.cjs.map +1 -1
  5. package/dist/primary/shell/src/UI/components/shell-header/shell-header.d.ts +1 -0
  6. package/dist/primary/shell/src/api/api.d.ts +2 -0
  7. package/dist/primary/shell/src/api/http-client/http-client.d.ts +3 -2
  8. package/dist/primary/shell/src/api/plugin-busy-manager/plugin-busy-list/component.d.ts +12 -0
  9. package/dist/primary/shell/src/api/plugin-busy-manager/plugin-busy-list/template.d.ts +3 -0
  10. package/dist/primary/shell/src/api/plugin-busy-manager/plugin-busy-manager.d.ts +19 -0
  11. package/dist/primary/shell/src/api/plugin-busy-manager/plugin-busy-manager.test.d.ts +1 -0
  12. package/dist/primary/shell/src/api/token-manager/token-manager.d.ts +1 -1
  13. package/dist/primary/shell/src/events.d.ts +1 -0
  14. package/dist/primary/shell/src/features/exit/bootstrapper.d.ts +2 -0
  15. package/dist/primary/shell/src/features/exit/handler.d.ts +10 -0
  16. package/dist/primary/shell/src/features/exit/request.d.ts +4 -0
  17. package/dist/primary/shell/src/handle-plugins.d.ts +2 -2
  18. package/dist/primary/shell/src/internal-plugins/activity-history/activity-history-item/add/add-async-history-items/validate-add-async-items-command.d.ts +4 -0
  19. package/dist/primary/shell/src/internal-plugins/activity-history/activity-history-item/add/add-history-items/reducer.d.ts +1 -1
  20. package/dist/primary/shell/src/internal-plugins/activity-history/activity-history-item/domain/model.d.ts +3 -0
  21. package/dist/primary/shell/src/internal-plugins/activity-history/activity-history-item/filter/UI/active-filters-badges/active-filters-badges.d.ts +4 -1
  22. package/dist/primary/shell/src/internal-plugins/activity-history/activity-history-item/filter/UI/active-filters-header/active-filters-header.d.ts +1 -2
  23. package/dist/primary/shell/src/internal-plugins/activity-history/activity-history-item/filter/common-filters/selectors.d.ts +152 -0
  24. package/dist/primary/shell/src/internal-plugins/activity-history/activity-history-item/filter/utils.d.ts +2 -0
  25. package/dist/primary/shell/src/internal-plugins/activity-history/activity-history-item/list/UI/timeline/activity-history-timeline.d.ts +2 -0
  26. package/dist/primary/shell/src/internal-plugins/activity-history/handle-views.d.ts +4 -0
  27. package/dist/primary/shell/src/internal-plugins/activity-history/infrastructure/ioc/container.d.ts +1 -0
  28. package/dist/primary/shell/src/internal-plugins/activity-history/infrastructure/ioc/types.d.ts +0 -1
  29. package/dist/primary/shell/src/internal-plugins/activity-history/localization.d.ts +1 -0
  30. package/dist/primary/shell/src/internal-plugins/activity-history/main.d.ts +0 -1
  31. package/dist/primary/shell/src/locales.d.ts +4 -0
  32. package/dist/style.css +1 -1
  33. package/package.json +2 -2
  34. package/src/UI/components/index.ts +3 -2
  35. package/src/UI/components/shell-header/shell-header.ts +5 -0
  36. package/src/UI/components/shell-header/template.ts +2 -2
  37. package/src/api/api.ts +10 -3
  38. package/src/api/broker/factory.ts +1 -1
  39. package/src/api/http-client/http-client.test.ts +60 -6
  40. package/src/api/http-client/http-client.ts +17 -10
  41. package/src/api/plugin-busy-manager/plugin-busy-list/component.ts +19 -0
  42. package/src/api/plugin-busy-manager/plugin-busy-list/styles.css +20 -0
  43. package/src/api/plugin-busy-manager/plugin-busy-list/template.ts +13 -0
  44. package/src/api/plugin-busy-manager/plugin-busy-manager.test.ts +49 -0
  45. package/src/api/plugin-busy-manager/plugin-busy-manager.ts +38 -0
  46. package/src/api/token-manager/token-manager.test.ts +0 -12
  47. package/src/api/token-manager/token-manager.ts +12 -7
  48. package/src/disposer.ts +0 -1
  49. package/src/events.ts +1 -0
  50. package/src/features/bootstrapper.ts +3 -0
  51. package/src/features/exit/bootstrapper.ts +17 -0
  52. package/src/features/exit/handler.ts +51 -0
  53. package/src/features/exit/request.ts +3 -0
  54. package/src/handle-plugins.ts +7 -6
  55. package/src/handle-views.ts +4 -1
  56. package/src/internal-plugins/activity-history/activity-history-item/add/add-async-history-items/handler.ts +2 -0
  57. package/src/internal-plugins/activity-history/activity-history-item/add/add-async-history-items/validate-add-async-items-command.ts +15 -0
  58. package/src/internal-plugins/activity-history/activity-history-item/add/add-history-items/reducer.ts +10 -7
  59. package/src/internal-plugins/activity-history/activity-history-item/domain/model.ts +4 -0
  60. package/src/internal-plugins/activity-history/activity-history-item/filter/UI/active-filters-badges/active-filters-badges.ts +13 -5
  61. package/src/internal-plugins/activity-history/activity-history-item/filter/UI/active-filters-badges/template.ts +26 -2
  62. package/src/internal-plugins/activity-history/activity-history-item/filter/UI/active-filters-header/active-filters-header.ts +5 -8
  63. package/src/internal-plugins/activity-history/activity-history-item/filter/UI/active-filters-header/template.ts +6 -1
  64. package/src/internal-plugins/activity-history/activity-history-item/filter/common-filters/selectors.ts +10 -0
  65. package/src/internal-plugins/activity-history/activity-history-item/filter/custom-filters/set-custom-filter-value/reducer.ts +7 -1
  66. package/src/internal-plugins/activity-history/activity-history-item/filter/utils.ts +9 -0
  67. package/src/internal-plugins/activity-history/activity-history-item/list/UI/timeline/activity-history-timeline.ts +30 -0
  68. package/src/internal-plugins/activity-history/activity-history-item/list/UI/timeline/template.ts +7 -4
  69. package/src/internal-plugins/activity-history/handle-views.ts +17 -0
  70. package/src/internal-plugins/activity-history/infrastructure/ioc/container.ts +5 -0
  71. package/src/internal-plugins/activity-history/infrastructure/ioc/types.ts +0 -1
  72. package/src/internal-plugins/activity-history/localization.ts +7 -8
  73. package/src/internal-plugins/activity-history/main.ts +4 -10
  74. package/src/locales.ts +8 -4
  75. package/dist/primary/shell/src/internal-plugins/activity-history/utils/get-locale-manager-dependency.d.ts +0 -1
  76. package/src/internal-plugins/activity-history/utils/get-locale-manager-dependency.ts +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uxland/primary-shell",
3
- "version": "4.2.0",
3
+ "version": "4.3.1",
4
4
  "description": "Primaria Shell",
5
5
  "author": "UXLand <dev@uxland.es>",
6
6
  "homepage": "https://github.com/uxland/harmonix/tree/app#readme",
@@ -41,7 +41,7 @@
41
41
  "axios-mock-adapter": "^2.0.0",
42
42
  "inversify": "^6.0.2",
43
43
  "inversify-inject-decorators": "^3.1.0",
44
- "@salut/design-system-salut": "../../design-system-salut-2.2.0.tgz",
44
+ "@salut/design-system-salut": "../../design-system-salut-2.2.1.tgz",
45
45
  "jwt-decode": "^4.0.0",
46
46
  "lit": "^3.1.0",
47
47
  "mediatr-ts": "^1.2.1",
@@ -1,5 +1,6 @@
1
1
  import "./primaria-shell/primaria-shell";
2
2
  import "./clinical-monitoring/clinical-monitoring";
3
3
  import "../shared-components";
4
- import "../components/shell-header/shell-header";
5
- import "../components/quick-actions-menu/quick-actions-menu";
4
+ import "./shell-header/shell-header";
5
+ import "./quick-actions-menu/quick-actions-menu";
6
+ import "../../api/plugin-busy-manager/plugin-busy-list/component";
@@ -7,6 +7,7 @@ import { GetUserInfo } from "../../../features/get-user-info/request";
7
7
  import styles from "./styles.css?inline";
8
8
  import { shellRegions } from "../../../api/region-manager/regions";
9
9
  import { IRegion, region } from "@uxland/regions";
10
+ import { ExitShell } from "../../../features/exit/request";
10
11
 
11
12
  @customElement("shell-header")
12
13
  export class ShellHeader extends PrimariaRegionHost(LitElement) {
@@ -34,6 +35,10 @@ export class ShellHeader extends PrimariaRegionHost(LitElement) {
34
35
  this.menuOpened = !this.menuOpened;
35
36
  }
36
37
 
38
+ logout() {
39
+ shellApi.broker.send(new ExitShell());
40
+ }
41
+
37
42
  connectedCallback() {
38
43
  super.connectedCallback();
39
44
  shellApi.broker.send(new GetUserInfo()).then((response: IUserInfo) => {
@@ -2,7 +2,7 @@ import { html } from "lit";
2
2
  import salutLogo from "../../../UI/images/Salut_Logotip.svg";
3
3
  import { ShellHeader } from "./shell-header";
4
4
  import { when } from "lit/directives/when.js";
5
- import {translate} from '../../../locales';
5
+ import { translate } from "../../../locales";
6
6
 
7
7
  export const template = (props: ShellHeader) => {
8
8
  const workCenterElements = [{ label: props.professional?.workCenter, value: "1" }];
@@ -21,7 +21,7 @@ export const template = (props: ShellHeader) => {
21
21
  <div id="header-actions-region-container"></div>
22
22
  ${when(
23
23
  props.professional,
24
- () => html`<dss-header-menu-professional slot="professional-menu" name="${props.professional.firstName} ${props.professional?.familyName} ${props.professional?.lastName}" center="${props.professional.workCenter}" collegiate="${props.professional.registrationNumber}">
24
+ () => html`<dss-header-menu-professional @onLogout=${props.logout} slot="professional-menu" name="${props.professional.firstName} ${props.professional?.familyName} ${props.professional?.lastName}" center="${props.professional.workCenter}" collegiate="${props.professional.registrationNumber}">
25
25
  <dss-avatar size="xl" name="${props.professional.firstName}" surname="${props.professional?.familyName}" slot="avatar"></dss-avatar>
26
26
  <dss-input-dropdown icon="maps_home_work" type="default" .elements=${workCenterElements} selectedvalue="[&quot;1&quot;]">
27
27
  <label slot="label" for="preferences1">${translate("header.workCenter")}</label>
package/src/api/api.ts CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  createRegionManager,
8
8
  } from "@uxland/harmonix";
9
9
  import { PrimariaRegionManager, createRegionManagerProxy } from "./region-manager/region-manager";
10
- import { createBroker } from "./broker/factory";
10
+ import { createBroker } from "./broker/factory";
11
11
  import { PrimariaBroker } from "./broker/primaria-broker";
12
12
  import { PrimariaGlobalStateManager, createGlobalStateManager } from "./global-state/global-state";
13
13
  import { HttpClient, createHttpClient } from "./http-client/http-client";
@@ -18,9 +18,13 @@ import {
18
18
  import { createLocaleManager } from "./localization/localization";
19
19
  import { TokenManager, createTokenManager } from "./token-manager/token-manager";
20
20
  import { EcapEventManager, createEcapEventManager } from "./ecap-event-manager/ecap-event-manager";
21
+ import {
22
+ PluginBusyManager,
23
+ PluginBusyManagerImpl,
24
+ } from "./plugin-busy-manager/plugin-busy-manager";
21
25
 
22
- const broker = createBroker();
23
26
 
27
+ const broker = createBroker();
24
28
  export interface PrimariaApi extends HarmonixApi {
25
29
  httpClient: HttpClient;
26
30
  interactionManager: PrimariaInteractionManager;
@@ -29,12 +33,14 @@ export interface PrimariaApi extends HarmonixApi {
29
33
  globalStateManager: PrimariaGlobalStateManager;
30
34
  tokenManager: TokenManager;
31
35
  ecapEventManager: EcapEventManager;
36
+ pluginBusyManager: PluginBusyManager;
32
37
  }
33
38
 
34
39
  const regionManager: IRegionManager = createRegionManager("primaria");
35
40
  export const PrimariaRegionHost: any = createRegionHost(regionManager as any);
36
41
  const tokenManager = createTokenManager();
37
42
  const globalStateManager: PrimariaGlobalStateManager = createGlobalStateManager(broker);
43
+ const pluginBusyManager = new PluginBusyManagerImpl();
38
44
 
39
45
  /**
40
46
  * Factory function that creates a Primaria API instance.
@@ -48,13 +54,14 @@ export const primariaApiFactory: ApiFactory<PrimariaApi> = (
48
54
  return {
49
55
  pluginInfo: pluginInfo,
50
56
  regionManager: createRegionManagerProxy(pluginInfo, regionManager, broker),
51
- httpClient: createHttpClient(tokenManager),
57
+ httpClient: createHttpClient(tokenManager, broker),
52
58
  interactionManager: { ...createInteractionManager() },
53
59
  broker: broker,
54
60
  createLocaleManager: createLocaleManager(pluginInfo.pluginId) as any,
55
61
  globalStateManager,
56
62
  tokenManager,
57
63
  ecapEventManager: createEcapEventManager(),
64
+ pluginBusyManager,
58
65
  };
59
66
  };
60
67
 
@@ -154,4 +154,4 @@ const createDynamicMessageHandler = (handler: messageHandler, classPrefix: strin
154
154
  )(handler);
155
155
  };
156
156
 
157
- export const createBroker = (): PrimariaBroker => new Broker();
157
+ export const createBroker = (): PrimariaBroker => new Broker();
@@ -3,24 +3,32 @@ import AxiosMockAdapter from "axios-mock-adapter";
3
3
  import { Mock, beforeEach, describe, expect, it, vi } from "vitest";
4
4
  import { TokenManager, createTokenManager } from "../token-manager/token-manager";
5
5
  import { createAxiosInstance } from "./http-client";
6
+ import { shellEvents } from "../../events";
7
+ import { createBroker } from "../broker/factory";
6
8
 
7
9
  const access_token = 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJidFZxNnRMWGpmcXdzbm5MR2FXMXdhdU9McDNiTmY4bWZ3Rm1SZ0lBS2VJIn0.eyJleHAiOjE3Mzg2NjcxODIsImlhdCI6MTczODY2NjI4MiwianRpIjoiY2I0M2M2ZmItY2MyMS00M2Y2LTk5M2UtNDM3ZTJjNDU3YWMzIiwiaXNzIjoiaHR0cHM6Ly9wcmVwcm9kdWNjaW8ucGRzLmhlcy5jYXRzYWx1dC5nZW5jYXQuY2F0L2F1dGgvcmVhbG1zL0hFUyIsInN1YiI6Ijk1OGUyZWQ5LTJkNmUtNDZjOC1hOTE5LTU5MDQ0ZTYwMzVjMiIsInR5cCI6IkJlYXJlciIsImF6cCI6ImV0Yy1jY2YtcHJlIiwic2Vzc2lvbl9zdGF0ZSI6ImU3MzIzYWIyLTU3MDktNGY1ZS05ZGU4LWU1MzM3ZTBhOTMxNiIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLWhlcyIsIlJPTEVfSEVTX0VUQyIsIlJPTEVfSEVTX0xBTkRJTkciXX0sInNjb3BlIjoiIiwic2lkIjoiZTczMjNhYjItNTcwOS00ZjVlLTlkZTgtZTUzMzdlMGE5MzE2IiwiY2xpZW50SG9zdCI6IjEwLjUzLjI1NC4xNTAiLCJhY2Nlc3NfcnVsZXMiOnt9LCJhY2Nlc3NfaW5mbyI6eyJtb2R1bGVfY29kZSI6IkEwMDEiLCJyb2xlX3R5cGUiOiJOT1JNIiwidHJhY2VfdXNlcl9pZCI6IlVzZXJfSUQiLCJjZW50ZXJfdHlwZSI6IkUiLCJ1cF9jb2RlIjoiMDc3MzMiLCJ0cmFjZV91c2VyX2dpdmVuX25hbWUiOiJHaXZlbiBOYW1lIiwidHJhY2VfdXNlcl9mYW1pbHlfbmFtZSI6IkZhbWlseSBOYW1lIiwidXNlcl90eXBlIjoiQURNIiwiY2VudGVyX2NvZGUiOiJFMDg1ODY5NjMiLCJwcm9mZXNzaW9uYWxfY2F0ZWdvcnkiOiIzMDkzNDMwMDYiLCJzZXJ2aWNlX2NvZGUiOiI1UzA4OSIsImVwX2NvZGUiOiIwMjA4IiwiaWRlbnRpZmllciI6W3sidHlwZSI6IkROSSIsInZhbHVlIjoiNzMyODgyMTlBIn0seyJ0eXBlIjoiTlVNQ09MIiwidmFsdWUiOiIyIn1dLCJhbHRfaWRlbnRpZmllciI6W3sidHlwZSI6Ik1QSSIsInZhbHVlIjoiMDYyMWNmN2QtN2Q2My00OWVjLTgwMDYtOGMwNTY5MmVkMzc3In1dfSwiY2xpZW50QWRkcmVzcyI6IjEwLjUzLjI1NC4xNTAiLCJjbGllbnRfaWQiOiJldGMtY2NmLXByZSJ9.WYF6VvIVqzW71MdOcvRJnKNBEe8BoD43Ql_hyGyqBC1wbNDNcbqucX-2wPbSOpHaHv5iogYNTVp1eEkHvrnryQ4koHXf4U3inTxoZR94qAzIt8XGgBVxpYlArc-XrFX1FAwkdfsNmYE_L6G7q5wfzaP0y1YrFH6u9TPTBl1w2us8O8xKPB6B652NUphNHFWmPWv6t6Zq97sjHPIHMZsDeBdolK5RlG5J3u8K-quJeKLByZhA2kYAJMGyCzR6jLLrup5w-WdYNJRyGopeFSDVp-lECmaiYIXmhQOJzzJQqhARrwYN8ZxqTOyD5u24HOB7Q1ZCMnAp4vAL6OphWyRZuw';
8
10
  const refresh_token = 'eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIwMjUzZGM1MC1hY2FmLTQ5ZDctYTYzNi0xN2NkMTlmOWEwOTAifQ.eyJleHAiOjE3Mzg2NjgwODIsImlhdCI6MTczODY2NjI4MiwianRpIjoiZWM5OGE2OWMtMDJkNS00Yjc0LWIwZWYtNzcwMGU4OTc3YWY1IiwiaXNzIjoiaHR0cHM6Ly9wcmVwcm9kdWNjaW8ucGRzLmhlcy5jYXRzYWx1dC5nZW5jYXQuY2F0L2F1dGgvcmVhbG1zL0hFUyIsImF1ZCI6Imh0dHBzOi8vcHJlcHJvZHVjY2lvLnBkcy5oZXMuY2F0c2FsdXQuZ2VuY2F0LmNhdC9hdXRoL3JlYWxtcy9IRVMiLCJzdWIiOiI5NThlMmVkOS0yZDZlLTQ2YzgtYTkxOS01OTA0NGU2MDM1YzIiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoiZXRjLWNjZi1wcmUiLCJzZXNzaW9uX3N0YXRlIjoiZTczMjNhYjItNTcwOS00ZjVlLTlkZTgtZTUzMzdlMGE5MzE2Iiwic2NvcGUiOiIiLCJzaWQiOiJlNzMyM2FiMi01NzA5LTRmNWUtOWRlOC1lNTMzN2UwYTkzMTYifQ.KOO0Ulbed4640-obEr3Xg8qavGeXhU25o6ZUfsq0SoQpzMjhcx8GTgOH-PiIR88ksrjtTnpbmzw2D29xuugv4A';
9
11
 
10
12
 
11
13
  describe("HTTP Client", () => {
12
- vi.spyOn(window, 'location', 'get').mockReturnValue({
13
- search: `?access_token=${access_token}&refresh_token=${refresh_token}`
14
- } as unknown as Location);
14
+
15
+ let broker = createBroker();
15
16
  let tokenManager: TokenManager;
16
17
  let axiosInstance: AxiosInstance;
17
18
  let axiosMockInstance: AxiosMockAdapter;
19
+
18
20
  beforeEach(() => {
21
+ vi.restoreAllMocks(); // Reset all spies before each test
22
+ vi.spyOn(window, 'location', 'get').mockReturnValue({
23
+ search: `?access_token=${access_token}&refresh_token=${refresh_token}`
24
+ } as unknown as Location);
25
+
19
26
  tokenManager = createTokenManager();
20
- axiosInstance = createAxiosInstance(tokenManager);
27
+ axiosInstance = createAxiosInstance(tokenManager, broker);
21
28
  axiosMockInstance = new AxiosMockAdapter(axiosInstance);
22
29
  axiosMockInstance.reset();
23
30
  });
31
+
24
32
  it("should make a request with header", async () => {
25
33
  axiosMockInstance.onGet("/api/clinical-course").reply(200, "success");
26
34
  // Perform the request
@@ -34,22 +42,68 @@ describe("HTTP Client", () => {
34
42
  `Bearer ${access_token}`,
35
43
  );
36
44
  });
37
- it("should retry request after token refresh", async () => {
45
+
46
+ it("should retry request with new token if failed with 401 after token refresh, and requests after that have new token", async () => {
47
+
38
48
  axiosMockInstance
39
49
  .onGet("/api/clinical-course")
40
50
  .replyOnce(401, "Unauthorized")
41
51
  .onGet("/api/clinical-course")
52
+ .replyOnce(200, "success")
53
+ .onGet("/api/clinical-course/2")
42
54
  .replyOnce(200, "success");
55
+
56
+ vi.spyOn(axios, "post").mockResolvedValueOnce({
57
+ data: {
58
+ access_token: "new-auth-token",
59
+ refresh_token: "new-refresh-token"
60
+ }
61
+ });
62
+
43
63
  // Perform the request
44
64
  const response = await axiosInstance.get("/api/clinical-course");
65
+ const secondResponse = await axiosInstance.get("/api/clinical-course/2");
45
66
 
46
67
  console.log("response", response.status);
47
68
  // Assertions
48
69
  expect(response.status).toBe(200);
49
70
  expect(response.data).toBe("success");
50
- expect(axiosMockInstance.history.get?.length).toBe(2);
71
+ expect(axiosMockInstance.history.get?.length).toBe(3); // 3 Calls 2 for the first request (request and retry), 1 for the second request
51
72
  expect(axiosMockInstance.history.get?.[1]?.headers?.Authorization).toBe(
52
73
  "Bearer new-auth-token",
53
74
  );
75
+ expect(axiosMockInstance.history.get?.[2]?.headers?.Authorization).toBe(
76
+ "Bearer new-auth-token",
77
+ );
78
+
79
+ });
80
+
81
+ it("should fail request if token refresh fails and publish an event", async () => {
82
+ vi.spyOn(axios, "post").mockRejectedValueOnce({
83
+ response: { status: 400, statusText: "Bad Request" },
84
+ });
85
+ const brokerSpy = vi.spyOn(broker, "publish")
86
+ axiosMockInstance.onGet("/api/clinical-course").replyOnce(401, "Unauthorized");
87
+
88
+ await expect(axiosInstance.get("/api/clinical-course")).rejects.toThrow("Request failed with status code 401");
89
+
90
+ expect(axiosMockInstance.history.get?.length).toBe(1); // No retry happened
91
+ expect(brokerSpy).toHaveBeenCalledWith(shellEvents.refreshTokenFailed, expect.any(Object));
92
+ expect(brokerSpy).toHaveBeenCalledWith(shellEvents.refreshTokenFailed, {
93
+ request: expect.objectContaining({ url: "/api/clinical-course" }),
94
+ });
95
+ });
96
+
97
+ it("should fail request with error != 401 and should not retry", async () => {
98
+ const refreshTokenSpy = vi.spyOn(tokenManager, "refreshToken");
99
+ const brokerSpy = vi.spyOn(broker, "publish");
100
+ axiosMockInstance.onGet("/api/clinical-course").replyOnce(500, "Internal Server Error");
101
+ await expect(axiosInstance.get("/api/clinical-course")).rejects.toMatchObject({
102
+ response: { status: 500 },
103
+ });
104
+ expect(axiosMockInstance.history.get?.length).toBe(1);
105
+ expect(refreshTokenSpy).not.toHaveBeenCalled();
106
+ expect(brokerSpy).not.toHaveBeenCalled();
54
107
  });
108
+
55
109
  });
@@ -1,12 +1,15 @@
1
1
  import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
2
2
  import { TokenManager } from "../token-manager/token-manager";
3
+ import { shellEvents } from "../../events";
4
+ import { PrimariaBroker } from "../broker/primaria-broker";
3
5
 
4
- export const createAxiosInstance = (tokenManager: TokenManager) => {
5
- const instance = axios.create({
6
- headers: {
7
- Authorization: `Bearer ${tokenManager.getToken()}`,
8
- },
6
+ export const createAxiosInstance = (tokenManager: TokenManager, broker: PrimariaBroker) => {
7
+ const instance = axios.create();
8
+ instance.interceptors.request.use((config) => {
9
+ config.headers.Authorization = `Bearer ${tokenManager.getToken()}`;
10
+ return config;
9
11
  });
12
+
10
13
  instance.interceptors.response.use(
11
14
  (response: AxiosResponse) => response,
12
15
  async (error) => {
@@ -14,9 +17,15 @@ export const createAxiosInstance = (tokenManager: TokenManager) => {
14
17
  if (error.response.status === 401 && !originalRequest._retry) {
15
18
  originalRequest._retry = true;
16
19
  // Implement token refresh logic here and update the token in headers
20
+ try {
17
21
  const newToken = await tokenManager.refreshToken();
18
22
  originalRequest.headers.Authorization = `Bearer ${newToken}`;
19
23
  return instance(originalRequest);
24
+ } catch (refreshError) {
25
+ console.error("Error refreshing token:", refreshError);
26
+ broker.publish(shellEvents.refreshTokenFailed, {request: originalRequest});
27
+ return Promise.reject(error);
28
+ }
20
29
  }
21
30
  return Promise.reject(error);
22
31
  },
@@ -29,14 +38,12 @@ let instance;
29
38
 
30
39
  //TODO: Test concurrent request that get intercepted by expired token and
31
40
  // An option is to use subscribers to enque unauth requests and only refresh one token and retry the others with the new token
32
-
33
-
34
41
  export interface HttpClient {
35
42
  request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
36
43
  }
37
- export const createHttpClient = (tokenManager: TokenManager): HttpClient => {
44
+ export const createHttpClient = (tokenManager: TokenManager, broker: PrimariaBroker): HttpClient => {
38
45
  if (!instance) {
39
- instance = createAxiosInstance(tokenManager);
46
+ instance = createAxiosInstance(tokenManager, broker);
40
47
  }
41
48
  return { request: instance.request };
42
- };
49
+ };
@@ -0,0 +1,19 @@
1
+ import { LitElement, css, html, unsafeCSS } from "lit";
2
+ import { customElement } from "lit/decorators.js";
3
+ import styles from "./styles.css?inline";
4
+ import { template } from "./template";
5
+ import { confirmMixin } from "../../../UI/shared-components/primaria-interaction/confirm-mixin";
6
+ import { PluginBusyTask } from "../plugin-busy-manager";
7
+
8
+ @customElement("plugin-busy-list")
9
+ export class PluginBusyList extends confirmMixin(LitElement) {
10
+ static styles = css`
11
+ ${unsafeCSS(styles)}
12
+ `;
13
+
14
+ render() {
15
+ return html`${template(this)}`;
16
+ }
17
+
18
+ model: { busyTasks: PluginBusyTask[] };
19
+ }
@@ -0,0 +1,20 @@
1
+ .container{
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 4px;
5
+ align-items: center;
6
+ }
7
+
8
+ .title{
9
+ font-size: 15px;
10
+ line-height: 24px;
11
+ font-weight: 600;
12
+ color: var(--color-red-600);
13
+ }
14
+
15
+
16
+ .list{
17
+ display: flex;
18
+ flex-direction: column;
19
+ gap: 8px;
20
+ }
@@ -0,0 +1,13 @@
1
+ import { html } from "lit";
2
+ import { PluginBusyList } from "./component";
3
+ import { PluginBusyTask } from "../plugin-busy-manager";
4
+ import { translate } from "../../../locales";
5
+
6
+ export const template = (props: PluginBusyList) => html`
7
+ <div class="container">
8
+ <div class="title">${translate("busyManager.title")}</div>
9
+ <div class="list">
10
+ ${props.model?.busyTasks?.map((item: PluginBusyTask) => html`<div class="plugin-busy-item">${item.taskDescription}</div>`)}
11
+ </div>
12
+ </div>
13
+ `;
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { PluginBusyManagerImpl } from "./plugin-busy-manager";
3
+
4
+ describe("PluginBusyManagerImpl", () => {
5
+ let manager: PluginBusyManagerImpl;
6
+
7
+ beforeEach(() => {
8
+ manager = new PluginBusyManagerImpl();
9
+ });
10
+
11
+ it("should start with no busy plugins", () => {
12
+ expect(manager.getBusyPluginTasks()).toEqual([]);
13
+ expect(manager.isAnyPluginBusy()).toBe(false);
14
+ });
15
+
16
+ it("should add a busy plugin", () => {
17
+ const task = { taskId: "plugin1", taskDescription: "Description for plugin1" };
18
+ manager.addBusyPluginTask(task);
19
+ expect(manager.getBusyPluginTasks()).toContainEqual(task);
20
+ expect(manager.isAnyPluginBusy()).toBe(true);
21
+ });
22
+
23
+ it("should remove a busy plugin", () => {
24
+ const task1 = { taskId: "plugin1", taskDescription: "Description for plugin1" };
25
+ const task2 = { taskId: "plugin2", taskDescription: "Description for plugin2" };
26
+ manager.addBusyPluginTask(task1);
27
+ manager.addBusyPluginTask(task2);
28
+ manager.removeBusyPluginTask("plugin1");
29
+ expect(manager.getBusyPluginTasks()).not.toContainEqual(task1);
30
+ expect(manager.getBusyPluginTasks()).toContainEqual(task2);
31
+ });
32
+
33
+ it("should do nothing if removing a non-existent plugin", () => {
34
+ const task = { taskId: "plugin1", taskDescription: "Description for plugin1" };
35
+ manager.addBusyPluginTask(task);
36
+ manager.removeBusyPluginTask("nonexistent");
37
+ expect(manager.getBusyPluginTasks()).toEqual([task]);
38
+ });
39
+
40
+ it("should clear all busy plugins", () => {
41
+ const task1 = { taskId: "plugin1", taskDescription: "Description for plugin1" };
42
+ const task2 = { taskId: "plugin2", taskDescription: "Description for plugin2" };
43
+ manager.addBusyPluginTask(task1);
44
+ manager.addBusyPluginTask(task2);
45
+ manager.clearAllBusyPlugins();
46
+ expect(manager.getBusyPluginTasks()).toEqual([]);
47
+ expect(manager.isAnyPluginBusy()).toBe(false);
48
+ });
49
+ });
@@ -0,0 +1,38 @@
1
+ export interface PluginBusyTask {
2
+ taskId: string;
3
+ taskDescription: string;
4
+ }
5
+
6
+ export abstract class PluginBusyManager {
7
+ abstract addBusyPluginTask(busyTask: PluginBusyTask): void;
8
+ abstract removeBusyPluginTask(taskId: string): any;
9
+ abstract clearAllBusyPlugins(): void;
10
+ abstract isAnyPluginBusy(): boolean;
11
+ abstract getBusyPluginTasks(): PluginBusyTask[];
12
+ }
13
+ export class PluginBusyManagerImpl implements PluginBusyManager {
14
+ private busyPluginTasks: PluginBusyTask[] = [];
15
+
16
+ public addBusyPluginTask(busyTask: PluginBusyTask): void {
17
+ this.busyPluginTasks.push(busyTask);
18
+ }
19
+
20
+ public removeBusyPluginTask(taskId: string): any {
21
+ const index = this.busyPluginTasks.findIndex((item) => item.taskId === taskId);
22
+ if (index > -1) {
23
+ this.busyPluginTasks.splice(index, 1);
24
+ }
25
+ }
26
+
27
+ public isAnyPluginBusy(): boolean {
28
+ return this.busyPluginTasks.length > 0;
29
+ }
30
+
31
+ public clearAllBusyPlugins(): void {
32
+ this.busyPluginTasks = [];
33
+ }
34
+
35
+ public getBusyPluginTasks(): PluginBusyTask[] {
36
+ return this.busyPluginTasks;
37
+ }
38
+ }
@@ -13,17 +13,5 @@ describe("Token Manager test", () => {
13
13
  it("should return initial token", () => {
14
14
  const tokenManager = createTokenManager();
15
15
  expect(tokenManager.getToken()).toBe(access_token);
16
- });
17
-
18
- it("should refresh token", async () => {
19
- const tokenManager = createTokenManager();
20
- const refreshPromise = tokenManager.refreshToken();
21
- const oldToken = tokenManager.getToken();
22
- const newToken = await refreshPromise;
23
- const newToken2 = tokenManager.getToken();
24
- expect(newToken).toBe("new-auth-token");
25
- expect(newToken).toBe(newToken2);
26
- expect(newToken).not.toBe(oldToken);
27
- expect(oldToken).toBe(access_token);
28
16
  });
29
17
  });
@@ -1,3 +1,5 @@
1
+ import axios from "axios";
2
+
1
3
  export interface TokenManager {
2
4
  getToken: () => string;
3
5
  refreshToken: () => Promise<string>;
@@ -5,7 +7,7 @@ export interface TokenManager {
5
7
 
6
8
  let token: string;
7
9
  let refreshToken: string;
8
- export class TokenManagerSimulator implements TokenManager {
10
+ export class TokenManagerImpl implements TokenManager {
9
11
  getUrlParams = (): URLSearchParams => {
10
12
  return new URLSearchParams(window.location.search);
11
13
  };
@@ -14,7 +16,6 @@ export class TokenManagerSimulator implements TokenManager {
14
16
  const searchString = this.getUrlParams();
15
17
  token = searchString.get("access_token") || "";
16
18
  refreshToken = searchString.get("refresh_token") || "";
17
-
18
19
  return token;
19
20
  };
20
21
 
@@ -23,16 +24,20 @@ export class TokenManagerSimulator implements TokenManager {
23
24
  };
24
25
 
25
26
  refreshToken = async () => {
26
- //sleep for 1 second change token and return token
27
- await new Promise((resolve) => setTimeout(resolve, 200));
28
- token = "new-auth-token";
29
- return token;
27
+ const response = await axios.post('/api/token/refresh', {token: refreshToken});
28
+ const {access_token, refresh_token} = response.data;
29
+ if(!access_token){
30
+ throw new Error("Invalid refresh token response");
31
+ }
32
+ token = access_token;
33
+ refreshToken = refresh_token;
34
+ return token;
30
35
  };
31
36
  }
32
37
  let tokenManager;
33
38
  export const createTokenManager = () => {
34
39
  if(tokenManager) return tokenManager;
35
- tokenManager = new TokenManagerSimulator();
40
+ tokenManager = new TokenManagerImpl();
36
41
  tokenManager.initToken();
37
42
  return tokenManager;
38
43
  }
package/src/disposer.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { shellApi } from "./api/api";
2
2
  import { teardownFeatures } from "./features/bootstrapper";
3
3
  export const disposeShell = () => {
4
- shellApi.ecapEventManager.publish("CLOSE_REQUESTED_RECEIVED", "", {});
5
4
  teardownFeatures();
6
5
  };
7
6
 
package/src/events.ts CHANGED
@@ -2,4 +2,5 @@ export const shellEvents = {
2
2
  openClinicalMonitoringRequested: "openClinicalMonitoringRequested",
3
3
  appCrashed: "appCrashed",
4
4
  mainViewChanged: "mainViewChanged",
5
+ refreshTokenFailed: "refreshTokenFailed",
5
6
  };
@@ -1,16 +1,19 @@
1
1
  import { PrimariaApi, shellApi } from "../api/api";
2
2
  import { container } from "../infrastructure/ioc/container";
3
3
  import { TYPES } from "../infrastructure/ioc/types";
4
+ import { bootstrapExitShell, teardownExitShell } from "./exit/bootstrapper";
4
5
  import { bootstrapGetUserInfo, teardownGetUserInfo } from "./get-user-info/bootstrapper";
5
6
  import { GetUserInfo } from "./get-user-info/request";
6
7
 
7
8
  export const bootstrapFeatures = (api: PrimariaApi) => {
8
9
  container.bind(TYPES.primaryApi).toConstantValue(api);
9
10
  bootstrapGetUserInfo();
11
+ bootstrapExitShell();
10
12
  shellApi.broker.send(new GetUserInfo());
11
13
  };
12
14
 
13
15
  export const teardownFeatures = () => {
14
16
  teardownGetUserInfo();
17
+ teardownExitShell();
15
18
  container.unbindAll();
16
19
  };
@@ -0,0 +1,17 @@
1
+ import { shellApi } from "../../api/api";
2
+ import { BrokerDisposableHandler } from "../../api/broker/primaria-broker";
3
+ import { container } from "../../infrastructure/ioc/container";
4
+ import { registerRequest } from "../utils";
5
+ import { ExitShellHandler } from "./handler";
6
+ import { ExitShell } from "./request";
7
+
8
+ let request: BrokerDisposableHandler;
9
+
10
+ export const bootstrapExitShell = () => {
11
+ teardownExitShell();
12
+ request = registerRequest(shellApi, container)(ExitShell, ExitShellHandler);
13
+ };
14
+
15
+ export const teardownExitShell = () => {
16
+ request?.dispose();
17
+ };
@@ -0,0 +1,51 @@
1
+ import { CustomConfirmOptions, PrimariaApi } from "@uxland/primary-shell";
2
+ import { TYPES } from "../../infrastructure/ioc/types";
3
+ import { inject } from "inversify";
4
+ import { ExitShell } from "./request";
5
+ import { disposeShell, raiseCloseEvent } from "../../disposer";
6
+ import { disposePlugins } from "../../handle-plugins";
7
+ import { PluginBusyTask } from "../../api/plugin-busy-manager/plugin-busy-manager";
8
+ import { PluginBusyList } from "../../api/plugin-busy-manager/plugin-busy-list/component";
9
+ import { translate } from "../../locales";
10
+
11
+ export class ExitShellHandler {
12
+ constructor(@inject(TYPES.primaryApi) private api: PrimariaApi) {}
13
+ async handle(message: ExitShell): Promise<void> {
14
+ try {
15
+ const busyTasks = this.api.pluginBusyManager.getBusyPluginTasks();
16
+ if (busyTasks.length > 0) {
17
+ const { confirmed } = await this.askForClose(busyTasks);
18
+ if (!confirmed) return;
19
+ }
20
+ disposeShell();
21
+
22
+ // Per si un plugin tarda molt en fer dispose, màxim deixarem 5 segons, per no interrompre el tancar infinitament
23
+ await Promise.race([
24
+ disposePlugins(), // S'intenta executar un dispose normal
25
+ this.timeout(5000), // Si passen 5s, es segueix amb l'execució
26
+ ]);
27
+
28
+ raiseCloseEvent();
29
+ } catch (error) {
30
+ this.api.interactionManager.notify({
31
+ type: "error",
32
+ message: error.message,
33
+ });
34
+ }
35
+ }
36
+
37
+ private askForClose(busyTasks: PluginBusyTask[]) {
38
+ return this.api.interactionManager.customConfirm({
39
+ title: translate("actions.askExit"),
40
+ componentConstructor: PluginBusyList,
41
+ type: "danger",
42
+ model: { busyTasks },
43
+ acceptLabel: "Si",
44
+ cancelLabel: "No",
45
+ } as CustomConfirmOptions<{ busyTasks: PluginBusyTask[] }>);
46
+ }
47
+
48
+ private timeout(ms: number) {
49
+ return new Promise<void>((resolve) => setTimeout(resolve, ms));
50
+ }
51
+ }
@@ -0,0 +1,3 @@
1
+ import { IRequest } from "@uxland/primary-shell";
2
+
3
+ export class ExitShell implements IRequest<void> {}
@@ -1,11 +1,13 @@
1
1
  import { bootstrapPlugins as pluginBootstrapper } from "@uxland/harmonix";
2
- import type { PluginDefinition, Plugin as PluginType } from "@uxland/harmonix";
2
+ import type { PluginDefinition, Plugin as PluginType, BootstrappedPlugin } from "@uxland/harmonix";
3
3
  export type { PluginDefinition, PluginInfo } from "@uxland/harmonix";
4
4
  import {
5
5
  initialize as activityHistoryInitialize,
6
6
  dispose as activityHistoryDispose,
7
7
  } from "./internal-plugins/activity-history/main";
8
- import { PrimariaApi, primariaApiFactory, shellApi } from "./api/api";
8
+ import { PrimariaApi, primariaApiFactory } from "./api/api";
9
+
10
+ let bootstrappedPlugins = [] as BootstrappedPlugin[];
9
11
 
10
12
  const internalPlugins: PluginDefinition[] = [
11
13
  {
@@ -20,12 +22,11 @@ const internalPlugins: PluginDefinition[] = [
20
22
 
21
23
  export const bootstrapPlugins = async (plugins: PluginDefinition[]) => {
22
24
  const finalPlugins = internalPlugins.concat(plugins || []);
23
- const bootstrappedPlugins = await pluginBootstrapper(finalPlugins, primariaApiFactory);
24
- return bootstrappedPlugins as Plugin[];
25
+ bootstrappedPlugins = await pluginBootstrapper(finalPlugins, primariaApiFactory);
25
26
  };
26
27
 
27
- export const disposePlugins = async (plugins: Plugin[]) => {
28
- return Promise.all(plugins.map((plugin) => plugin.dispose(shellApi)));
28
+ export const disposePlugins = async () => {
29
+ return Promise.all(bootstrappedPlugins.map((plugin: BootstrappedPlugin) => plugin.dispose()));
29
30
  };
30
31
 
31
32
  export type Plugin = PluginType<PrimariaApi>;