@uxland/primary-shell 7.27.0 → 7.29.0

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.
@@ -0,0 +1,2 @@
1
+ export { showNavItemTooltip } from './tooltip-manager';
2
+ export { NavigationTooltip } from './navigation-tooltip';
@@ -0,0 +1,8 @@
1
+ import { LitElement } from 'lit';
2
+ export declare class NavigationTooltip extends LitElement {
3
+ text: string;
4
+ itemAbsoluteY: number;
5
+ static styles: import('lit').CSSResult;
6
+ render(): import('lit').TemplateResult<1>;
7
+ }
8
+ export declare const renderNavigationTooltip: (text: string, itemAbsoluteY: number) => void;
@@ -0,0 +1 @@
1
+ export declare const showNavItemTooltip: (navItemMenuKey: string, text: string) => Promise<void>;
@@ -24,5 +24,6 @@ export declare class PrimariaShell extends PrimariaShell_base {
24
24
  message: string;
25
25
  }): void;
26
26
  _unsubscribeEvents(): void;
27
+ _scrollToNavItem(navItemMenuKey: string): Promise<void>;
27
28
  }
28
29
  export {};
@@ -4,4 +4,6 @@ export declare const shellEvents: {
4
4
  refreshTokenFailed: string;
5
5
  mpidHeaderInvalid: string;
6
6
  quickActionBusyChanged: string;
7
+ scrollToNavItemRequested: string;
8
+ scrollToNavItemCompleted: string;
7
9
  };
@@ -39,6 +39,7 @@ export declare const locales: {
39
39
  navButtonLabel: string;
40
40
  missingData: string;
41
41
  duplicatedSource: string;
42
+ tooltipMessage: string;
42
43
  };
43
44
  pdfVisor: {
44
45
  noPdfSelected: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uxland/primary-shell",
3
- "version": "7.27.0",
3
+ "version": "7.29.0",
4
4
  "description": "Primaria Shell",
5
5
  "author": "UXLand <dev@uxland.es>",
6
6
  "homepage": "https://github.com/uxland/harmonix/tree/app#readme",
@@ -0,0 +1,2 @@
1
+ export { showNavItemTooltip } from "./tooltip-manager";
2
+ export { NavigationTooltip } from "./navigation-tooltip";
@@ -0,0 +1,50 @@
1
+ import { LitElement, html, css, unsafeCSS } from "lit";
2
+ import { property } from "lit/decorators.js";
3
+ import styles from "./styles.css?inline";
4
+
5
+ export class NavigationTooltip extends LitElement {
6
+ @property({ type: String }) text = "";
7
+ @property({ type: Number }) itemAbsoluteY = 0;
8
+
9
+ static styles = css`
10
+ ${unsafeCSS(styles)}
11
+ `;
12
+
13
+ render() {
14
+ const itemHeight = 51;
15
+ const centerY = this.itemAbsoluteY + itemHeight / 2;
16
+
17
+ return html`
18
+ <div class="tooltip-overlay">
19
+ <div class="navigation-tooltip" style="left: 73px; top: ${centerY}px">
20
+ <dss-icon icon="info" size="md"></dss-icon>
21
+ ${this.text}
22
+ <div class="arrow"></div>
23
+ </div>
24
+ </div>
25
+ `;
26
+ }
27
+ }
28
+
29
+ customElements.define("navigation-tooltip", NavigationTooltip);
30
+
31
+ export const renderNavigationTooltip = (text: string, itemAbsoluteY: number) => {
32
+ // Remove existing tooltip if any
33
+ const existing = document.querySelector("navigation-tooltip");
34
+ if (existing) {
35
+ existing.remove();
36
+ }
37
+
38
+ // Create new tooltip
39
+ const tooltip = document.createElement("navigation-tooltip") as NavigationTooltip;
40
+ tooltip.text = text;
41
+ tooltip.itemAbsoluteY = itemAbsoluteY;
42
+ document.body.appendChild(tooltip);
43
+
44
+ // Auto-remove after 5 seconds
45
+ setTimeout(() => {
46
+ if (tooltip && document.body.contains(tooltip)) {
47
+ tooltip.remove();
48
+ }
49
+ }, 5000);
50
+ };
@@ -0,0 +1,67 @@
1
+ :host {
2
+ display: block;
3
+ }
4
+
5
+ .tooltip-overlay {
6
+ position: fixed;
7
+ top: 0;
8
+ left: 0;
9
+ width: 100vw;
10
+ height: 100vh;
11
+ z-index: 9999;
12
+ pointer-events: none;
13
+ }
14
+
15
+ .navigation-tooltip {
16
+ position: absolute;
17
+ min-width: 250px;
18
+ transform: translateY(-50%);
19
+ background: #EAF7FD;
20
+ color: #0F4877;
21
+ border: 2px solid #0F4877;
22
+ padding: 12px 16px;
23
+ border-radius: 10px;
24
+ white-space: nowrap;
25
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
26
+ font-size: 13px;
27
+ font-family: var(--etc-font-family, inherit);
28
+ font-weight: 500;
29
+ animation: fadeInOut 5s ease-in-out forwards;
30
+ display: flex;
31
+ align-items: center;
32
+ gap: 8px;
33
+ }
34
+
35
+ .tooltip-icon {
36
+ font-size: 16px;
37
+ }
38
+
39
+ .arrow {
40
+ position: absolute;
41
+ right: 100%;
42
+ top: 50%;
43
+ transform: translateY(-50%);
44
+ width: 0;
45
+ height: 0;
46
+ border: 12px solid transparent;
47
+ border-right-color: #0F4877;
48
+ }
49
+
50
+ @keyframes fadeInOut {
51
+ 0% {
52
+ opacity: 0;
53
+ transform: translateY(-50%) translateX(-10px);
54
+ }
55
+ 10% {
56
+ opacity: 1;
57
+ transform: translateY(-50%) translateX(0px);
58
+ }
59
+ 90% {
60
+ opacity: 1;
61
+ transform: translateY(-50%) translateX(0px);
62
+ }
63
+ 100% {
64
+ opacity: 0;
65
+ transform: translateY(-50%) translateX(-10px);
66
+ }
67
+ }
@@ -0,0 +1,31 @@
1
+ import { shellApi } from "../../../api/api";
2
+ import { shellEvents } from "../../../events";
3
+ import { renderNavigationTooltip } from "./navigation-tooltip";
4
+
5
+ const scrollToNavItem = (
6
+ navItemMenuKey: string,
7
+ ): Promise<{ scrollTop: number; containerTop: number; itemIndex: number; itemAbsoluteY: number }> => {
8
+ return new Promise((resolve) => {
9
+ const subscription = shellApi.broker.subscribe(
10
+ shellEvents.scrollToNavItemCompleted,
11
+ (data: { scrollTop: number; containerTop: number; itemIndex: number; itemAbsoluteY: number }) => {
12
+ subscription.dispose();
13
+ resolve(data);
14
+ },
15
+ );
16
+
17
+ shellApi.broker.publish(shellEvents.scrollToNavItemRequested, navItemMenuKey);
18
+
19
+ // Fallback timeout in case the event doesn't arrive
20
+ setTimeout(() => {
21
+ subscription.dispose();
22
+ resolve({ scrollTop: 0, containerTop: 0, itemIndex: -1, itemAbsoluteY: window.innerHeight / 2 });
23
+ }, 1000);
24
+ });
25
+ };
26
+
27
+ export const showNavItemTooltip = async (navItemMenuKey: string, text: string) => {
28
+ const scrollData = await scrollToNavItem(navItemMenuKey);
29
+
30
+ renderNavigationTooltip(text, scrollData.itemAbsoluteY);
31
+ };
@@ -89,6 +89,12 @@ export class PrimariaShell extends PrimariaRegionHost(LitElement) {
89
89
  this.quickActionBusy = detail.busy;
90
90
  }),
91
91
  );
92
+
93
+ this.subscriptions.push(
94
+ shellApi.broker.subscribe(shellEvents.scrollToNavItemRequested, (navItemMenuKey: string) => {
95
+ this._scrollToNavItem(navItemMenuKey);
96
+ }),
97
+ );
92
98
  }
93
99
 
94
100
  _handleError(error: { message: string }) {
@@ -100,4 +106,88 @@ export class PrimariaShell extends PrimariaRegionHost(LitElement) {
100
106
  _unsubscribeEvents() {
101
107
  this.subscriptions.forEach((s) => s.dispose());
102
108
  }
109
+
110
+ async _scrollToNavItem(navItemMenuKey: string) {
111
+ const region = await shellApi.regionManager.getRegion(shellApi.regionManager.regions.shell.navigationMenu);
112
+
113
+ const allViews = region.currentActiveViews;
114
+
115
+ // Extract the actual view id from the key (remove plugin prefix)
116
+ // navItemMenuKey comes as "primaria-shell::pdf-viewer", we need just "pdf-viewer"
117
+ const viewId = navItemMenuKey.includes("::") ? navItemMenuKey.split("::")[1] : navItemMenuKey;
118
+
119
+ const targetView = allViews.find((view: any) => view.id === viewId);
120
+
121
+ if (!targetView) {
122
+ shellApi.broker.publish(shellEvents.scrollToNavItemCompleted, {
123
+ scrollTop: 0,
124
+ containerTop: 0,
125
+ itemIndex: -1,
126
+ });
127
+ return;
128
+ }
129
+
130
+ // Sort views by sortHint to find the correct position
131
+ const sortedViews = [...allViews].sort((a: any, b: any) => {
132
+ const sortHintA = a.sortHint || "999";
133
+ const sortHintB = b.sortHint || "999";
134
+ return sortHintA.localeCompare(sortHintB);
135
+ });
136
+
137
+ const targetIndex = sortedViews.findIndex((view: any) => view.id === viewId);
138
+
139
+ if (targetIndex === -1) {
140
+ shellApi.broker.publish(shellEvents.scrollToNavItemCompleted, {
141
+ scrollTop: 0,
142
+ containerTop: 0,
143
+ itemIndex: -1,
144
+ });
145
+ return;
146
+ }
147
+
148
+ const menuContainer = this.shadowRoot?.querySelector("#menu-region-container") as HTMLElement;
149
+ if (!menuContainer) {
150
+ shellApi.broker.publish(shellEvents.scrollToNavItemCompleted, {
151
+ scrollTop: 0,
152
+ containerTop: 0,
153
+ itemIndex: targetIndex,
154
+ });
155
+ return;
156
+ }
157
+
158
+ const itemHeight = 51;
159
+ const targetPosition = itemHeight * targetIndex;
160
+ const containerHeight = menuContainer.clientHeight;
161
+
162
+ // Scroll to position the item in the middle of the container
163
+ const scrollPosition = targetPosition - containerHeight / 2 + itemHeight / 2;
164
+ menuContainer.scrollTo({
165
+ top: Math.max(0, scrollPosition),
166
+ behavior: "smooth",
167
+ });
168
+
169
+ // Wait for scroll to complete and then publish the completed event
170
+ setTimeout(() => {
171
+ const containerRect = menuContainer.getBoundingClientRect();
172
+
173
+ // Get all nav items from the container
174
+ const navItems = Array.from(menuContainer.children) as HTMLElement[];
175
+
176
+ // Find the actual DOM element for the target item
177
+ let itemAbsoluteY = window.innerHeight / 2; // default fallback
178
+
179
+ if (navItems[targetIndex]) {
180
+ const itemRect = navItems[targetIndex].getBoundingClientRect();
181
+ itemAbsoluteY = itemRect.top;
182
+ }
183
+
184
+ const data = {
185
+ scrollTop: menuContainer.scrollTop,
186
+ containerTop: containerRect.top,
187
+ itemIndex: targetIndex,
188
+ itemAbsoluteY: itemAbsoluteY,
189
+ };
190
+ shellApi.broker.publish(shellEvents.scrollToNavItemCompleted, data);
191
+ }, 300);
192
+ }
103
193
  }
@@ -3,6 +3,8 @@ import { translate } from "../../locales";
3
3
  import { PrimariaNavItem } from "../../UI/shared-components/primaria-nav-item/primaria-nav-item";
4
4
  import { shellApi } from "../api";
5
5
  import { PdfVisor } from "./pdf-visor/pdf-visor";
6
+ import { showNavItemTooltip } from "../../UI/components/navigation-tooltip/tooltip-manager";
7
+ import { primariaShellId } from "../../constants";
6
8
 
7
9
  export const registerPdfViewerNavItem = () => {
8
10
  shellApi.regionManager.registerView(shellApi.regionManager.regions.shell.navigationMenu, {
@@ -18,6 +20,12 @@ export const registerPdfViewerNavItem = () => {
18
20
  return Promise.resolve(menuItem);
19
21
  },
20
22
  });
23
+
24
+ // Show tooltip when PDF viewer is registered
25
+ setTimeout(() => {
26
+ const navItemKey = `${primariaShellId}::${pdfViewerId}`;
27
+ showNavItemTooltip(navItemKey, translate("pdfManager.tooltipMessage"));
28
+ }, 100);
21
29
  };
22
30
 
23
31
  export const registerPDFVisorMainView = () => {
@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
  import { createPdfViewerManager } from "./pdf-viewer-manager";
3
3
  import { pdfViwerEvents } from "./events";
4
4
  import { registerPdfViewerNavItem } from "./handle-views";
5
+ import { showNavItemTooltip } from "../../UI/components/navigation-tooltip";
5
6
 
6
7
  vi.mock("@primaria/plugins-core", () => ({
7
8
  generateId: () => "generated-id",
@@ -15,6 +16,10 @@ vi.mock("../../locales", () => ({
15
16
  translate: (key: string) => key,
16
17
  }));
17
18
 
19
+ vi.mock("../../UI/components/navigation-tooltip", () => ({
20
+ showNavItemTooltip: vi.fn(),
21
+ }));
22
+
18
23
  describe("PdfViewerManager", () => {
19
24
  let manager: ReturnType<typeof createPdfViewerManager>;
20
25
  let broker: any;
@@ -60,6 +65,22 @@ describe("PdfViewerManager", () => {
60
65
  });
61
66
  });
62
67
 
68
+ it("should show tooltip when adding a valid pdf", async () => {
69
+ vi.useFakeTimers();
70
+
71
+ manager.add("Doc1", { url: "https://test.com/doc1.pdf" });
72
+
73
+ // Fast-forward the setTimeout(100ms)
74
+ vi.advanceTimersByTime(100);
75
+
76
+ expect(showNavItemTooltip).toHaveBeenCalledWith(
77
+ "primaria-shell::pdf-viewer",
78
+ "pdfManager.tooltipMessage"
79
+ );
80
+
81
+ vi.useRealTimers();
82
+ });
83
+
63
84
  it("should warn if pdf with same name already exists", () => {
64
85
  manager.add("Doc1", { b64: "xxx" });
65
86
  manager.add("Doc1", { b64: "xxx" });
@@ -68,6 +89,38 @@ describe("PdfViewerManager", () => {
68
89
  expect(manager.getPdfs()).toHaveLength(1); // solo el primero
69
90
  });
70
91
 
92
+ it("should not show tooltip when pdf with same name already exists", () => {
93
+ vi.useFakeTimers();
94
+
95
+ // First add succeeds and shows tooltip
96
+ manager.add("Doc1", { b64: "xxx" });
97
+ vi.advanceTimersByTime(100);
98
+
99
+ // Clear mocks after first add
100
+ vi.clearAllMocks();
101
+
102
+ // Second add with same name should show warning but NOT tooltip
103
+ manager.add("Doc1", { b64: "xxx" });
104
+ vi.advanceTimersByTime(100);
105
+
106
+ expect(notificationService.warning).toHaveBeenCalledWith("pdfManager.alreadyUploaded");
107
+ expect(showNavItemTooltip).not.toHaveBeenCalled();
108
+
109
+ vi.useRealTimers();
110
+ });
111
+
112
+ it("should not show tooltip when pdf data is invalid", () => {
113
+ vi.useFakeTimers();
114
+
115
+ manager.add("Doc1", {});
116
+ vi.advanceTimersByTime(100);
117
+
118
+ expect(notificationService.error).toHaveBeenCalledWith("pdfManager.missingData");
119
+ expect(showNavItemTooltip).not.toHaveBeenCalled();
120
+
121
+ vi.useRealTimers();
122
+ });
123
+
71
124
  it("should call registerNavButton only once", () => {
72
125
  manager.add("Doc1", { url: "u" });
73
126
  manager.add("Doc2", { url: "u2" });
@@ -7,6 +7,9 @@ import { PrimariaNotificationService } from "../notification-service/notificatio
7
7
  import { pdfViwerEvents } from "./events";
8
8
  import { PdfSelector } from "./pdf-visor/pdf-selector/pdf-selector";
9
9
  import { PdfVisor } from "./pdf-visor/pdf-visor";
10
+ import { showNavItemTooltip } from "../../UI/components/navigation-tooltip";
11
+ import { pdfViewerId } from "./constants";
12
+ import { primariaShellId } from "../../constants";
10
13
 
11
14
  export interface IPdfDocument {
12
15
  pdfName: string;
@@ -59,6 +62,12 @@ export class PdfViewerManager {
59
62
  this.broker.publish(pdfViwerEvents.added, pdf);
60
63
 
61
64
  this.notificationService.success(translate("pdfManager.uploaded"));
65
+
66
+ // Show tooltip to guide user to the PDF viewer
67
+ setTimeout(() => {
68
+ const navItemKey = `${primariaShellId}::${pdfViewerId}`;
69
+ showNavItemTooltip(navItemKey, translate("pdfManager.tooltipMessage"));
70
+ }, 100);
62
71
  }
63
72
  }
64
73
 
@@ -81,7 +90,5 @@ export class PdfViewerManager {
81
90
  }
82
91
  }
83
92
 
84
- export const createPdfViewerManager = (
85
- broker: PrimariaBroker,
86
- notificationService: PrimariaNotificationService,
87
- ) => new PdfViewerManager(broker, notificationService);
93
+ export const createPdfViewerManager = (broker: PrimariaBroker, notificationService: PrimariaNotificationService) =>
94
+ new PdfViewerManager(broker, notificationService);
package/src/events.ts CHANGED
@@ -4,4 +4,6 @@ export const shellEvents = {
4
4
  refreshTokenFailed: "refreshTokenFailed",
5
5
  mpidHeaderInvalid: "mpidHeaderInvalid",
6
6
  quickActionBusyChanged: "quickActionBusyChanged",
7
+ scrollToNavItemRequested: "scrollToNavItemRequested",
8
+ scrollToNavItemCompleted: "scrollToNavItemCompleted",
7
9
  };
package/src/locales.ts CHANGED
@@ -41,7 +41,7 @@ export const locales = {
41
41
  errors: {
42
42
  session: "Hi ha hagut un error amb la sessió. Siusplau, tanca i torna a obrir l'aplicació.",
43
43
  invalidPatient: "El pacient actual no és vàlid. Siusplau, tanca i torna a obrir l'aplicació.",
44
- exit: "Hi ha hagut un error en sortir de l'ETC"
44
+ exit: "Hi ha hagut un error en sortir de l'ETC",
45
45
  },
46
46
  header: {
47
47
  workCenter: "Centre treball",
@@ -57,17 +57,18 @@ export const locales = {
57
57
  navButtonLabel: "Visor PDF",
58
58
  missingData: "Es necesita un document o URL per enviar al visor de resultats",
59
59
  duplicatedSource: "Només pots envar un document o URL a la vegada",
60
+ tooltipMessage: "S'ha generat el PDF al visor",
60
61
  },
61
62
  pdfVisor: {
62
63
  noPdfSelected: "No hi ha cap PDF seleccionat",
63
64
  },
64
- importDataManager:{
65
+ importDataManager: {
65
66
  title: "Importar dades",
66
67
  actions: {
67
68
  cancel: "Cancel·lar",
68
69
  import: "Importar dades",
69
- }
70
- }
70
+ },
71
+ },
71
72
  },
72
73
  },
73
74
  };