@uxland/primary-shell 7.41.2 → 7.41.4

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.
@@ -60,6 +60,31 @@ export declare class PrimariaRegion extends PrimariaRegion_base {
60
60
  * Sets up the region definition before the parent connectedCallback runs.
61
61
  */
62
62
  connectedCallback(): void;
63
+ /**
64
+ * Mixin hook fired after `createRegions` finishes.
65
+ *
66
+ * Hydrates the freshly-created region with every view that has already been
67
+ * registered for this region name via `regionManager.registerViewWithRegion`.
68
+ * `registerViewWithRegion` only forwards to regions that exist at the moment
69
+ * of the call, so a plugin that registers once at `initialize` would lose
70
+ * its view every time the region is destroyed and re-created (e.g. when a
71
+ * drawer that hosts the region is closed and reopened). Pulling from the
72
+ * registry here makes injection transparent: any plugin can register once
73
+ * and any region with that name auto-populates.
74
+ */
75
+ regionsCreated(_regions: unknown): void;
76
+ /**
77
+ * Called when the component is removed from the DOM.
78
+ *
79
+ * The base mixin's `disconnectedCallback` removes the region from the manager
80
+ * but does NOT clear `this.region`, so on a later reconnect the mixin's
81
+ * `createRegions` sees `isNil(this.region) === false` and skips re-creating
82
+ * the region. That breaks the "drawer opens, closes, opens again" flow:
83
+ * the second open would render the host but never receive the plugin's
84
+ * view. Clear the reference (and drop the orphan container) so the next
85
+ * connect rebuilds everything from scratch.
86
+ */
87
+ disconnectedCallback(): void;
63
88
  /**
64
89
  * Called before the component updates.
65
90
  * Updates the region definition if the name changes.
@@ -1,9 +1,9 @@
1
1
  import { LitElement } from 'lit';
2
- import { PluginBusyTask } from '../plugin-busy-manager';
2
+ import { PluginTask } from '../plugin-busy-manager';
3
3
  export declare class PluginBusyList extends LitElement {
4
4
  static styles: import('lit').CSSResult;
5
5
  render(): import('lit').TemplateResult<1>;
6
6
  data: {
7
- busyTasks: PluginBusyTask[];
7
+ busyTasks: PluginTask[];
8
8
  };
9
9
  }
@@ -5,5 +5,6 @@ export declare class ExitShellHandler {
5
5
  constructor(api: PrimariaApi);
6
6
  handle(exitEvent: ExitShell): Promise<void>;
7
7
  private timeout;
8
+ private askForClose;
8
9
  private emitClose;
9
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uxland/primary-shell",
3
- "version": "7.41.2",
3
+ "version": "7.41.4",
4
4
  "description": "Primaria Shell",
5
5
  "author": "UXLand <dev@uxland.es>",
6
6
  "homepage": "https://github.com/uxland/harmonix/tree/app#readme",
@@ -1,6 +1,6 @@
1
1
  import { LitElement, html } from "lit";
2
2
  import { property } from "lit/decorators.js";
3
- import { IRegion } from "@uxland/regions";
3
+ import { IRegion, regionManager } from "@uxland/regions";
4
4
  import { PrimariaRegionHost } from "../../../api/api";
5
5
  import { regionsProperty } from "@uxland/regions/region-decorator";
6
6
 
@@ -130,6 +130,47 @@ export class PrimariaRegion extends PrimariaRegionHost(LitElement) {
130
130
  }
131
131
  }
132
132
 
133
+ /**
134
+ * Mixin hook fired after `createRegions` finishes.
135
+ *
136
+ * Hydrates the freshly-created region with every view that has already been
137
+ * registered for this region name via `regionManager.registerViewWithRegion`.
138
+ * `registerViewWithRegion` only forwards to regions that exist at the moment
139
+ * of the call, so a plugin that registers once at `initialize` would lose
140
+ * its view every time the region is destroyed and re-created (e.g. when a
141
+ * drawer that hosts the region is closed and reopened). Pulling from the
142
+ * registry here makes injection transparent: any plugin can register once
143
+ * and any region with that name auto-populates.
144
+ */
145
+ regionsCreated(_regions: unknown): void {
146
+ if (!this.region || !this.name) return;
147
+ const registered = regionManager.getRegisteredViews(this.name) ?? [];
148
+ for (const { key, view } of registered) {
149
+ if (this.region.containsView(key)) continue;
150
+ this.region.addView(key, view).catch((e) => {
151
+ console.warn(`primaria-region(${this.name}): failed to addView "${key}"`, e);
152
+ });
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Called when the component is removed from the DOM.
158
+ *
159
+ * The base mixin's `disconnectedCallback` removes the region from the manager
160
+ * but does NOT clear `this.region`, so on a later reconnect the mixin's
161
+ * `createRegions` sees `isNil(this.region) === false` and skips re-creating
162
+ * the region. That breaks the "drawer opens, closes, opens again" flow:
163
+ * the second open would render the host but never receive the plugin's
164
+ * view. Clear the reference (and drop the orphan container) so the next
165
+ * connect rebuilds everything from scratch.
166
+ */
167
+ disconnectedCallback(): void {
168
+ super.disconnectedCallback();
169
+ this.region = undefined;
170
+ const targetId = `${this.name}-container`;
171
+ this.querySelector(`#${targetId}`)?.remove();
172
+ }
173
+
133
174
  /**
134
175
  * Called before the component updates.
135
176
  * Updates the region definition if the name changes.
@@ -1,5 +1,5 @@
1
1
  import { LitElement, css, html, unsafeCSS } from "lit";
2
- import { PluginBusyTask } from "../plugin-busy-manager";
2
+ import { PluginTask } from "../plugin-busy-manager";
3
3
  import styles from "./styles.css?inline";
4
4
  import { template } from "./template";
5
5
 
@@ -12,5 +12,5 @@ export class PluginBusyList extends LitElement {
12
12
  return html`${template(this)}`;
13
13
  }
14
14
 
15
- data: { busyTasks: PluginBusyTask[] };
15
+ data: { busyTasks: PluginTask[] };
16
16
  }
@@ -1,14 +1,14 @@
1
1
  import { html } from "lit";
2
- import { PluginBusyList } from "./component";
3
- import { PluginBusyTask } from "../plugin-busy-manager";
4
2
  import { translate } from "../../../locales";
3
+ import { PluginTask } from "../plugin-busy-manager";
4
+ import { PluginBusyList } from "./component";
5
5
 
6
6
  export const template = (props: PluginBusyList) => html`
7
7
  <div class="container">
8
8
  <div class="title">${translate("busyManager.title")}</div>
9
9
  <div class="list">
10
10
  ${props.data?.busyTasks?.map(
11
- (item: PluginBusyTask) => html`
11
+ (item: PluginTask) => html`
12
12
  <div class="plugin-busy-item">
13
13
  <dss-typography tag="div" variant="body-3" fontweight="regular">
14
14
  ${item.taskDescription}
@@ -1,9 +1,10 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { PrimariaApi } from "../../api/api";
3
+ import { PluginBusyList } from "../../api/plugin-busy-manager/plugin-busy-list/component";
4
+ import { disposeShell, raiseCloseEvent, raiseCustomCloseEvent } from "../../disposer";
5
+ import { disposePlugins } from "../../handle-plugins";
2
6
  import { ExitShellHandler } from "./handler";
3
7
  import { ExitShell } from "./request";
4
- import { disposePlugins } from "../../handle-plugins";
5
- import { disposeShell, raiseCloseEvent, raiseCustomCloseEvent } from "../../disposer";
6
- import { PrimariaApi } from "../../api/api";
7
8
 
8
9
  vi.mock("../../handle-plugins", () => ({
9
10
  disposePlugins: vi.fn(),
@@ -18,7 +19,13 @@ vi.mock("../../disposer", () => ({
18
19
  const createMockApi = (): PrimariaApi =>
19
20
  ({
20
21
  exitGuardManager: {
21
- canExit: vi.fn(),
22
+ canExit: vi.fn().mockResolvedValue(true),
23
+ },
24
+ pluginBusyManager: {
25
+ getTasks: vi.fn().mockReturnValue([]),
26
+ },
27
+ interactionService: {
28
+ confirm: vi.fn(),
22
29
  },
23
30
  notificationService: {
24
31
  error: vi.fn(),
@@ -120,4 +127,57 @@ describe("ExitShellHandler", () => {
120
127
  expect(raiseCloseEvent).toHaveBeenCalled();
121
128
  expect(raiseCustomCloseEvent).not.toHaveBeenCalled();
122
129
  });
130
+
131
+ describe("legacy pluginBusyManager fallback", () => {
132
+ it("asks for confirmation when canExit allows but legacy busy tasks exist", async () => {
133
+ const busyTasks = [{ taskId: "t1", taskDescription: "saving draft" }];
134
+ const message = { ecapEvent: {} } as ExitShell;
135
+ (mockApi.exitGuardManager.canExit as any).mockResolvedValue(true);
136
+ (mockApi.pluginBusyManager.getTasks as any).mockReturnValue(busyTasks);
137
+ (mockApi.interactionService.confirm as any).mockResolvedValue({ confirmed: true });
138
+ (disposePlugins as any).mockResolvedValue(undefined);
139
+
140
+ await handler.handle(message);
141
+
142
+ expect(mockApi.interactionService.confirm).toHaveBeenCalledWith(
143
+ { busyTasks },
144
+ { component: PluginBusyList },
145
+ {
146
+ title: "actions.askExit",
147
+ state: "error",
148
+ confirmButtonText: "Sí",
149
+ cancelButtonText: "No",
150
+ },
151
+ );
152
+ expect(disposePlugins).toHaveBeenCalled();
153
+ expect(disposeShell).toHaveBeenCalled();
154
+ expect(raiseCustomCloseEvent).toHaveBeenCalledWith(message);
155
+ });
156
+
157
+ it("aborts the exit when the legacy busy-task confirmation is declined", async () => {
158
+ const busyTasks = [{ taskId: "t1", taskDescription: "saving draft" }];
159
+ const message = { ecapEvent: {} } as ExitShell;
160
+ (mockApi.exitGuardManager.canExit as any).mockResolvedValue(true);
161
+ (mockApi.pluginBusyManager.getTasks as any).mockReturnValue(busyTasks);
162
+ (mockApi.interactionService.confirm as any).mockResolvedValue({ confirmed: false });
163
+
164
+ await handler.handle(message);
165
+
166
+ expect(mockApi.interactionService.confirm).toHaveBeenCalled();
167
+ expect(disposePlugins).not.toHaveBeenCalled();
168
+ expect(disposeShell).not.toHaveBeenCalled();
169
+ expect(raiseCustomCloseEvent).not.toHaveBeenCalled();
170
+ expect(raiseCloseEvent).not.toHaveBeenCalled();
171
+ });
172
+
173
+ it("does not consult legacy busy tasks when an ExitGuard already vetoed the exit", async () => {
174
+ const message = { ecapEvent: {} } as ExitShell;
175
+ (mockApi.exitGuardManager.canExit as any).mockResolvedValue(false);
176
+
177
+ await handler.handle(message);
178
+
179
+ expect(mockApi.pluginBusyManager.getTasks).not.toHaveBeenCalled();
180
+ expect(mockApi.interactionService.confirm).not.toHaveBeenCalled();
181
+ });
182
+ });
123
183
  });
@@ -1,10 +1,12 @@
1
- import { PrimariaApi } from "../../api/api";
2
- import { TYPES } from "../../infrastructure/ioc/types";
3
1
  import { inject } from "inversify";
4
- import { ExitShell } from "./request";
2
+ import { PrimariaApi } from "../../api/api";
3
+ import { PluginBusyList } from "../../api/plugin-busy-manager/plugin-busy-list/component";
4
+ import { PluginTask } from "../../api/plugin-busy-manager/plugin-busy-manager";
5
5
  import { disposeShell, raiseCloseEvent, raiseCustomCloseEvent } from "../../disposer";
6
6
  import { disposePlugins } from "../../handle-plugins";
7
+ import { TYPES } from "../../infrastructure/ioc/types";
7
8
  import { translate } from "../../locales";
9
+ import { ExitShell } from "./request";
8
10
 
9
11
  export class ExitShellHandler {
10
12
  constructor(@inject(TYPES.primaryApi) private api: PrimariaApi) {}
@@ -15,6 +17,14 @@ export class ExitShellHandler {
15
17
  const canExit = await this.api.exitGuardManager.canExit();
16
18
  if (!canExit) return;
17
19
 
20
+ // Backwards-compat: external plugins still using the deprecated pluginBusyManager
21
+ // would otherwise lose their exit-confirmation modal. Remove once all consumers migrate.
22
+ const busyTasks = this.api.pluginBusyManager.getTasks();
23
+ if (busyTasks.length > 0) {
24
+ const { confirmed } = await this.askForClose(busyTasks);
25
+ if (!confirmed) return;
26
+ }
27
+
18
28
  // Per si un plugin tarda molt en fer dispose, màxim deixarem 5 segons, per no interrompre el tancar infinitament
19
29
  await Promise.race([
20
30
  disposePlugins(), // S'intenta executar un dispose normal
@@ -32,6 +42,19 @@ export class ExitShellHandler {
32
42
  return new Promise<void>((resolve) => setTimeout(resolve, ms));
33
43
  }
34
44
 
45
+ private askForClose(busyTasks: PluginTask[]) {
46
+ return this.api.interactionService.confirm(
47
+ { busyTasks },
48
+ { component: PluginBusyList },
49
+ {
50
+ title: translate("actions.askExit"),
51
+ state: "error",
52
+ confirmButtonText: "Sí",
53
+ cancelButtonText: "No",
54
+ },
55
+ );
56
+ }
57
+
35
58
  private emitClose(exitEvent?: ExitShell): void {
36
59
  if (exitEvent) {
37
60
  raiseCustomCloseEvent(exitEvent);