@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.
- package/dist/index.js +19452 -19117
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +1681 -1549
- package/dist/index.umd.cjs.map +1 -1
- package/dist/primary/shell/src/UI/components/shell-header/shell-header.d.ts +1 -0
- package/dist/primary/shell/src/api/api.d.ts +2 -0
- package/dist/primary/shell/src/api/http-client/http-client.d.ts +3 -2
- package/dist/primary/shell/src/api/plugin-busy-manager/plugin-busy-list/component.d.ts +12 -0
- package/dist/primary/shell/src/api/plugin-busy-manager/plugin-busy-list/template.d.ts +3 -0
- package/dist/primary/shell/src/api/plugin-busy-manager/plugin-busy-manager.d.ts +19 -0
- package/dist/primary/shell/src/api/plugin-busy-manager/plugin-busy-manager.test.d.ts +1 -0
- package/dist/primary/shell/src/api/token-manager/token-manager.d.ts +1 -1
- package/dist/primary/shell/src/events.d.ts +1 -0
- package/dist/primary/shell/src/features/exit/bootstrapper.d.ts +2 -0
- package/dist/primary/shell/src/features/exit/handler.d.ts +10 -0
- package/dist/primary/shell/src/features/exit/request.d.ts +4 -0
- package/dist/primary/shell/src/handle-plugins.d.ts +2 -2
- 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
- package/dist/primary/shell/src/internal-plugins/activity-history/activity-history-item/add/add-history-items/reducer.d.ts +1 -1
- package/dist/primary/shell/src/internal-plugins/activity-history/activity-history-item/domain/model.d.ts +3 -0
- package/dist/primary/shell/src/internal-plugins/activity-history/activity-history-item/filter/UI/active-filters-badges/active-filters-badges.d.ts +4 -1
- package/dist/primary/shell/src/internal-plugins/activity-history/activity-history-item/filter/UI/active-filters-header/active-filters-header.d.ts +1 -2
- package/dist/primary/shell/src/internal-plugins/activity-history/activity-history-item/filter/common-filters/selectors.d.ts +152 -0
- package/dist/primary/shell/src/internal-plugins/activity-history/activity-history-item/filter/utils.d.ts +2 -0
- package/dist/primary/shell/src/internal-plugins/activity-history/activity-history-item/list/UI/timeline/activity-history-timeline.d.ts +2 -0
- package/dist/primary/shell/src/internal-plugins/activity-history/handle-views.d.ts +4 -0
- package/dist/primary/shell/src/internal-plugins/activity-history/infrastructure/ioc/container.d.ts +1 -0
- package/dist/primary/shell/src/internal-plugins/activity-history/infrastructure/ioc/types.d.ts +0 -1
- package/dist/primary/shell/src/internal-plugins/activity-history/localization.d.ts +1 -0
- package/dist/primary/shell/src/internal-plugins/activity-history/main.d.ts +0 -1
- package/dist/primary/shell/src/locales.d.ts +4 -0
- package/dist/style.css +1 -1
- package/package.json +2 -2
- package/src/UI/components/index.ts +3 -2
- package/src/UI/components/shell-header/shell-header.ts +5 -0
- package/src/UI/components/shell-header/template.ts +2 -2
- package/src/api/api.ts +10 -3
- package/src/api/broker/factory.ts +1 -1
- package/src/api/http-client/http-client.test.ts +60 -6
- package/src/api/http-client/http-client.ts +17 -10
- package/src/api/plugin-busy-manager/plugin-busy-list/component.ts +19 -0
- package/src/api/plugin-busy-manager/plugin-busy-list/styles.css +20 -0
- package/src/api/plugin-busy-manager/plugin-busy-list/template.ts +13 -0
- package/src/api/plugin-busy-manager/plugin-busy-manager.test.ts +49 -0
- package/src/api/plugin-busy-manager/plugin-busy-manager.ts +38 -0
- package/src/api/token-manager/token-manager.test.ts +0 -12
- package/src/api/token-manager/token-manager.ts +12 -7
- package/src/disposer.ts +0 -1
- package/src/events.ts +1 -0
- package/src/features/bootstrapper.ts +3 -0
- package/src/features/exit/bootstrapper.ts +17 -0
- package/src/features/exit/handler.ts +51 -0
- package/src/features/exit/request.ts +3 -0
- package/src/handle-plugins.ts +7 -6
- package/src/handle-views.ts +4 -1
- package/src/internal-plugins/activity-history/activity-history-item/add/add-async-history-items/handler.ts +2 -0
- package/src/internal-plugins/activity-history/activity-history-item/add/add-async-history-items/validate-add-async-items-command.ts +15 -0
- package/src/internal-plugins/activity-history/activity-history-item/add/add-history-items/reducer.ts +10 -7
- package/src/internal-plugins/activity-history/activity-history-item/domain/model.ts +4 -0
- package/src/internal-plugins/activity-history/activity-history-item/filter/UI/active-filters-badges/active-filters-badges.ts +13 -5
- package/src/internal-plugins/activity-history/activity-history-item/filter/UI/active-filters-badges/template.ts +26 -2
- package/src/internal-plugins/activity-history/activity-history-item/filter/UI/active-filters-header/active-filters-header.ts +5 -8
- package/src/internal-plugins/activity-history/activity-history-item/filter/UI/active-filters-header/template.ts +6 -1
- package/src/internal-plugins/activity-history/activity-history-item/filter/common-filters/selectors.ts +10 -0
- package/src/internal-plugins/activity-history/activity-history-item/filter/custom-filters/set-custom-filter-value/reducer.ts +7 -1
- package/src/internal-plugins/activity-history/activity-history-item/filter/utils.ts +9 -0
- package/src/internal-plugins/activity-history/activity-history-item/list/UI/timeline/activity-history-timeline.ts +30 -0
- package/src/internal-plugins/activity-history/activity-history-item/list/UI/timeline/template.ts +7 -4
- package/src/internal-plugins/activity-history/handle-views.ts +17 -0
- package/src/internal-plugins/activity-history/infrastructure/ioc/container.ts +5 -0
- package/src/internal-plugins/activity-history/infrastructure/ioc/types.ts +0 -1
- package/src/internal-plugins/activity-history/localization.ts +7 -8
- package/src/internal-plugins/activity-history/main.ts +4 -10
- package/src/locales.ts +8 -4
- package/dist/primary/shell/src/internal-plugins/activity-history/utils/get-locale-manager-dependency.d.ts +0 -1
- 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.
|
|
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.
|
|
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 "
|
|
5
|
-
import "
|
|
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
|
|
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="["1"]">
|
|
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 {
|
|
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
|
|
|
@@ -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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
40
|
+
tokenManager = new TokenManagerImpl();
|
|
36
41
|
tokenManager.initToken();
|
|
37
42
|
return tokenManager;
|
|
38
43
|
}
|
package/src/disposer.ts
CHANGED
package/src/events.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/handle-plugins.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
24
|
-
return bootstrappedPlugins as Plugin[];
|
|
25
|
+
bootstrappedPlugins = await pluginBootstrapper(finalPlugins, primariaApiFactory);
|
|
25
26
|
};
|
|
26
27
|
|
|
27
|
-
export const disposePlugins = async (
|
|
28
|
-
return Promise.all(
|
|
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>;
|