@xenide-io/the-old-ui-theme 0.9.13 → 0.9.15
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/suite.d.mts +47 -4
- package/dist/suite.d.ts +47 -4
- package/dist/suite.js +1099 -996
- package/dist/suite.mjs +696 -588
- package/package.json +1 -1
- package/src/suite/components/suite-integration-picker.test.tsx +36 -2
- package/src/suite/components/suite-integration-picker.tsx +104 -72
- package/src/suite/components/suite-notification-bell.test.tsx +87 -9
- package/src/suite/components/suite-notification-bell.tsx +29 -167
- package/src/suite/index.ts +7 -5
- package/src/suite/lib/suite-notifications.tsx +260 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xenide-io/the-old-ui-theme",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.15",
|
|
4
4
|
"description": "Props-driven React components and theme tokens inspired by classic interface design — optimized for Next.js apps and internal tools.",
|
|
5
5
|
"author": "Xenide",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,6 +46,39 @@ describe("SuiteIntegrationPicker", () => {
|
|
|
46
46
|
expect(screen.queryByText("Linear")).not.toBeInTheDocument();
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
it("filters to bridge-capable apps when Shelly Bridge only is on", async () => {
|
|
50
|
+
const user = userEvent.setup();
|
|
51
|
+
|
|
52
|
+
render(
|
|
53
|
+
<SuiteIntegrationPicker
|
|
54
|
+
open
|
|
55
|
+
items={[
|
|
56
|
+
{
|
|
57
|
+
id: "bridged",
|
|
58
|
+
label: "Bridged app",
|
|
59
|
+
description: "Can bridge into Tides",
|
|
60
|
+
bridgeTargets: ["tides"],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "shelly-only",
|
|
64
|
+
label: "Shelly only app",
|
|
65
|
+
description: "AI only",
|
|
66
|
+
},
|
|
67
|
+
]}
|
|
68
|
+
onClose={vi.fn()}
|
|
69
|
+
onConnect={vi.fn()}
|
|
70
|
+
/>,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(screen.getByText("Bridged app")).toBeInTheDocument();
|
|
74
|
+
expect(screen.getByText("Shelly only app")).toBeInTheDocument();
|
|
75
|
+
|
|
76
|
+
await user.click(screen.getByLabelText("Shelly Bridge only"));
|
|
77
|
+
|
|
78
|
+
expect(screen.getByText("Bridged app")).toBeInTheDocument();
|
|
79
|
+
expect(screen.queryByText("Shelly only app")).not.toBeInTheDocument();
|
|
80
|
+
});
|
|
81
|
+
|
|
49
82
|
it("shows auth tags and passes the selected method to the connector", async () => {
|
|
50
83
|
const user = userEvent.setup();
|
|
51
84
|
const onConnect = vi.fn();
|
|
@@ -59,6 +92,7 @@ describe("SuiteIntegrationPicker", () => {
|
|
|
59
92
|
label: "Mixed provider",
|
|
60
93
|
description: "Supports multiple connection methods",
|
|
61
94
|
categories: ["calendar", "email"],
|
|
95
|
+
bridgeTargets: ["turtletime", "tides"],
|
|
62
96
|
authSchemes: ["OAUTH2", "API_KEY"],
|
|
63
97
|
},
|
|
64
98
|
]}
|
|
@@ -69,8 +103,8 @@ describe("SuiteIntegrationPicker", () => {
|
|
|
69
103
|
|
|
70
104
|
expect(screen.getByText("OAuth 2.0")).toBeInTheDocument();
|
|
71
105
|
expect(screen.getByText("API key")).toBeInTheDocument();
|
|
72
|
-
expect(screen.
|
|
73
|
-
expect(screen.
|
|
106
|
+
expect(screen.getByTitle("Bridge in TurtleTime")).toBeInTheDocument();
|
|
107
|
+
expect(screen.getByTitle("Bridge in Tides")).toBeInTheDocument();
|
|
74
108
|
|
|
75
109
|
await user.click(screen.getByRole("button", { name: "Connect" }));
|
|
76
110
|
await user.click(screen.getByRole("button", { name: /API key Select/ }));
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ReactNode,
|
|
11
11
|
} from "react";
|
|
12
12
|
import {
|
|
13
|
+
Bridge3d,
|
|
13
14
|
EvPlug as Plug,
|
|
14
15
|
Filter,
|
|
15
16
|
NavArrowDown,
|
|
@@ -18,8 +19,6 @@ import {
|
|
|
18
19
|
Xmark,
|
|
19
20
|
} from "iconoir-react";
|
|
20
21
|
import { Button, Input, Modal } from "@/components/ui";
|
|
21
|
-
import { SUITE_APPS, SUITE_APP_MAP, type SuiteAppSlug } from "@/suite/lib/apps";
|
|
22
|
-
import { SuiteAppIcon } from "@/suite/icons";
|
|
23
22
|
|
|
24
23
|
export interface SuiteIntegrationPickerItem {
|
|
25
24
|
id: string;
|
|
@@ -27,6 +26,8 @@ export interface SuiteIntegrationPickerItem {
|
|
|
27
26
|
description: string;
|
|
28
27
|
icon?: ReactNode;
|
|
29
28
|
categories?: string[];
|
|
29
|
+
/** Product apps this toolkit can actually bridge. Empty means Shelly AI only. */
|
|
30
|
+
bridgeTargets?: string[];
|
|
30
31
|
authSchemes?: string[];
|
|
31
32
|
connecting?: boolean;
|
|
32
33
|
unavailableReason?: string;
|
|
@@ -63,6 +64,13 @@ export interface SuiteIntegrationPickerProps {
|
|
|
63
64
|
category?: string;
|
|
64
65
|
categories?: string[];
|
|
65
66
|
onCategoryChange?: (value: string) => void;
|
|
67
|
+
/**
|
|
68
|
+
* When provided, the "Shelly Bridge only" filter is controlled by the caller
|
|
69
|
+
* (so it can refetch a bridge-filtered catalogue server-side). When omitted,
|
|
70
|
+
* the picker filters the loaded items locally.
|
|
71
|
+
*/
|
|
72
|
+
bridgeOnly?: boolean;
|
|
73
|
+
onBridgeOnlyChange?: (value: boolean) => void;
|
|
66
74
|
onClose: () => void;
|
|
67
75
|
onConnect: (
|
|
68
76
|
item: SuiteIntegrationPickerItem,
|
|
@@ -87,7 +95,7 @@ function IntegrationSkeletonCard() {
|
|
|
87
95
|
aria-hidden="true"
|
|
88
96
|
>
|
|
89
97
|
<div className="flex items-start gap-3">
|
|
90
|
-
<div className="h-
|
|
98
|
+
<div className="h-8 w-8 shrink-0 rounded-md bg-ph-muted" />
|
|
91
99
|
<div className="min-w-0 flex-1 space-y-2 pt-1">
|
|
92
100
|
<div className="h-4 w-3/5 rounded bg-ph-muted" />
|
|
93
101
|
<div className="h-3 w-full rounded bg-ph-muted" />
|
|
@@ -128,46 +136,28 @@ function authSchemesFor(item: SuiteIntegrationPickerItem) {
|
|
|
128
136
|
);
|
|
129
137
|
}
|
|
130
138
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
* knowledge sources feed Kraken, CRM/finance feed the Portal. Unmapped
|
|
136
|
-
* categories default to Kraken's authorised-source search.
|
|
137
|
-
*/
|
|
138
|
-
const CATEGORY_APP_AFFINITY: Record<string, readonly SuiteAppSlug[]> = {
|
|
139
|
-
calendar: ["turtletime", "tides"],
|
|
140
|
-
email: ["turtletime", "tides"],
|
|
141
|
-
code: ["tides", "kraken"],
|
|
142
|
-
chat: ["tides", "kraken"],
|
|
143
|
-
documents: ["kraken"],
|
|
144
|
-
notes: ["kraken"],
|
|
145
|
-
knowledge: ["kraken"],
|
|
146
|
-
file_storage: ["kraken"],
|
|
147
|
-
crm: ["shellstack", "tides"],
|
|
148
|
-
finance: ["shellstack", "tides"],
|
|
149
|
-
payment: ["shellstack", "tides"],
|
|
150
|
-
monitoring: ["tides", "kraken"],
|
|
151
|
-
design: ["tides", "kraken"],
|
|
152
|
-
support: ["tides", "shellstack"],
|
|
153
|
-
ticketing: ["tides", "kraken"],
|
|
139
|
+
const BRIDGE_MARK: Record<string, { src: string; name: string }> = {
|
|
140
|
+
turtletime: { src: "/turtletime-icon.svg", name: "TurtleTime" },
|
|
141
|
+
tides: { src: "/tides-icon.svg", name: "Tides" },
|
|
142
|
+
kraken: { src: "/kraken-icon.svg", name: "Kraken" },
|
|
154
143
|
};
|
|
155
144
|
|
|
156
|
-
const DEFAULT_APP_AFFINITY: readonly SuiteAppSlug[] = ["tides", "kraken"];
|
|
157
|
-
|
|
158
145
|
export function suiteAppsForIntegration(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
146
|
+
itemOrCategories:
|
|
147
|
+
| { bridgeTargets?: readonly string[] }
|
|
148
|
+
| readonly string[]
|
|
149
|
+
| undefined,
|
|
150
|
+
): string[] {
|
|
151
|
+
if (
|
|
152
|
+
!itemOrCategories ||
|
|
153
|
+
Array.isArray(itemOrCategories) ||
|
|
154
|
+
!("bridgeTargets" in itemOrCategories)
|
|
155
|
+
) {
|
|
156
|
+
return [];
|
|
167
157
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
158
|
+
const explicit = itemOrCategories.bridgeTargets;
|
|
159
|
+
if (!Array.isArray(explicit)) return [];
|
|
160
|
+
return explicit.filter((app): app is string => app in BRIDGE_MARK);
|
|
171
161
|
}
|
|
172
162
|
|
|
173
163
|
export function SuiteIntegrationPicker({
|
|
@@ -184,6 +174,8 @@ export function SuiteIntegrationPicker({
|
|
|
184
174
|
category: controlledCategory,
|
|
185
175
|
categories: controlledCategories,
|
|
186
176
|
onCategoryChange,
|
|
177
|
+
bridgeOnly: controlledBridgeOnly,
|
|
178
|
+
onBridgeOnlyChange,
|
|
187
179
|
onClose,
|
|
188
180
|
onConnect,
|
|
189
181
|
onFetchAuthFields,
|
|
@@ -197,6 +189,7 @@ export function SuiteIntegrationPicker({
|
|
|
197
189
|
const [categoryQuery, setCategoryQuery] = useState("");
|
|
198
190
|
const [categoryMenuOpen, setCategoryMenuOpen] = useState(false);
|
|
199
191
|
const [expanded, setExpanded] = useState<string | null>(null);
|
|
192
|
+
const [bridgeOnlyInternal, setBridgeOnlyInternal] = useState(false);
|
|
200
193
|
const categoryMenuRef = useRef<HTMLDivElement>(null);
|
|
201
194
|
const categoryTriggerRef = useRef<HTMLButtonElement>(null);
|
|
202
195
|
const categorySearchRef = useRef<HTMLInputElement>(null);
|
|
@@ -207,6 +200,7 @@ export function SuiteIntegrationPicker({
|
|
|
207
200
|
const scrollViewportRef = useRef<HTMLDivElement>(null);
|
|
208
201
|
const loadMoreSentinelRef = useRef<HTMLDivElement>(null);
|
|
209
202
|
const loadMoreInFlightRef = useRef(false);
|
|
203
|
+
const sentinelVisibleRef = useRef(false);
|
|
210
204
|
const authRequestRef = useRef(0);
|
|
211
205
|
const [methodItem, setMethodItem] =
|
|
212
206
|
useState<SuiteIntegrationPickerItem | null>(null);
|
|
@@ -234,11 +228,16 @@ export function SuiteIntegrationPicker({
|
|
|
234
228
|
const categories = controlledCategories ?? itemCategories;
|
|
235
229
|
const activeSearch = controlledSearch ?? query;
|
|
236
230
|
const activeCategory = controlledCategory ?? category;
|
|
231
|
+
const bridgeOnly = controlledBridgeOnly ?? bridgeOnlyInternal;
|
|
232
|
+
const setBridgeOnly = (value: boolean) => {
|
|
233
|
+
setBridgeOnlyInternal(value);
|
|
234
|
+
onBridgeOnlyChange?.(value);
|
|
235
|
+
};
|
|
237
236
|
const categoryLabel = (value: string) =>
|
|
238
237
|
value
|
|
239
238
|
.replace(/[-_]/g, " ")
|
|
240
239
|
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
241
|
-
const
|
|
240
|
+
const catalogItems =
|
|
242
241
|
onSearchChange || onCategoryChange
|
|
243
242
|
? items
|
|
244
243
|
: items.filter((item) => {
|
|
@@ -250,6 +249,10 @@ export function SuiteIntegrationPicker({
|
|
|
250
249
|
(item.categories ?? []).includes(activeCategory))
|
|
251
250
|
);
|
|
252
251
|
});
|
|
252
|
+
const filtered =
|
|
253
|
+
bridgeOnly && controlledBridgeOnly === undefined
|
|
254
|
+
? catalogItems.filter((item) => (item.bridgeTargets ?? []).length > 0)
|
|
255
|
+
: catalogItems;
|
|
253
256
|
const visibleCategories = categories.filter((value) =>
|
|
254
257
|
categoryLabel(value)
|
|
255
258
|
.toLowerCase()
|
|
@@ -480,27 +483,37 @@ export function SuiteIntegrationPicker({
|
|
|
480
483
|
useEffect(() => {
|
|
481
484
|
const root = scrollViewportRef.current;
|
|
482
485
|
const sentinel = loadMoreSentinelRef.current;
|
|
483
|
-
if (
|
|
484
|
-
|
|
485
|
-
!root ||
|
|
486
|
-
!sentinel ||
|
|
487
|
-
!hasMore ||
|
|
488
|
-
!onEndReached ||
|
|
489
|
-
loading ||
|
|
490
|
-
catalogLoading ||
|
|
491
|
-
loadingMore
|
|
492
|
-
) {
|
|
486
|
+
if (!open || !root || !sentinel) {
|
|
487
|
+
sentinelVisibleRef.current = false;
|
|
493
488
|
return;
|
|
494
489
|
}
|
|
495
490
|
|
|
496
491
|
const observer = new IntersectionObserver(
|
|
497
492
|
([entry]) => {
|
|
498
|
-
|
|
493
|
+
// Only auto-load on the rising edge of the sentinel becoming visible.
|
|
494
|
+
// Without this, shrinking the list (search / category / Shelly Bridge
|
|
495
|
+
// only) leaves the sentinel pinned in view and each re-armed observer
|
|
496
|
+
// fires again, chain-loading the whole catalog page after page.
|
|
497
|
+
if (!entry.isIntersecting) {
|
|
498
|
+
sentinelVisibleRef.current = false;
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (sentinelVisibleRef.current) return;
|
|
502
|
+
sentinelVisibleRef.current = true;
|
|
503
|
+
if (
|
|
504
|
+
loadMoreInFlightRef.current ||
|
|
505
|
+
!hasMore ||
|
|
506
|
+
!onEndReached ||
|
|
507
|
+
loading ||
|
|
508
|
+
catalogLoading ||
|
|
509
|
+
loadingMore
|
|
510
|
+
) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
499
513
|
loadMoreInFlightRef.current = true;
|
|
500
|
-
observer.unobserve(entry.target);
|
|
501
514
|
onEndReached();
|
|
502
515
|
},
|
|
503
|
-
{ root, rootMargin: "0px 0px
|
|
516
|
+
{ root, rootMargin: "0px 0px 120px", threshold: 0 },
|
|
504
517
|
);
|
|
505
518
|
|
|
506
519
|
observer.observe(sentinel);
|
|
@@ -524,6 +537,24 @@ export function SuiteIntegrationPicker({
|
|
|
524
537
|
{connectionError}
|
|
525
538
|
</p>
|
|
526
539
|
) : null}
|
|
540
|
+
<label
|
|
541
|
+
className="inline-flex w-fit cursor-pointer items-center gap-2 text-sm text-ph-ink"
|
|
542
|
+
data-test={`${dataTest}-bridge-only`}
|
|
543
|
+
>
|
|
544
|
+
<input
|
|
545
|
+
type="checkbox"
|
|
546
|
+
role="switch"
|
|
547
|
+
className="ph-toggle"
|
|
548
|
+
checked={bridgeOnly}
|
|
549
|
+
onChange={(event) => setBridgeOnly(event.target.checked)}
|
|
550
|
+
aria-label="Shelly Bridge only"
|
|
551
|
+
/>
|
|
552
|
+
<Bridge3d
|
|
553
|
+
className="h-4 w-4 shrink-0 text-ph-subtle"
|
|
554
|
+
aria-hidden
|
|
555
|
+
/>
|
|
556
|
+
<span>Shelly Bridge only</span>
|
|
557
|
+
</label>
|
|
527
558
|
<div className="grid gap-4 md:grid-cols-2">
|
|
528
559
|
<div className="relative">
|
|
529
560
|
<Search
|
|
@@ -652,6 +683,8 @@ export function SuiteIntegrationPicker({
|
|
|
652
683
|
const isExpanded = expanded === item.id;
|
|
653
684
|
const longDescription =
|
|
654
685
|
item.description.length > DESCRIPTION_LIMIT;
|
|
686
|
+
const bridgeApps = suiteAppsForIntegration(item);
|
|
687
|
+
const schemes = authSchemesFor(item);
|
|
655
688
|
const text =
|
|
656
689
|
isExpanded || !longDescription
|
|
657
690
|
? item.description
|
|
@@ -671,9 +704,9 @@ export function SuiteIntegrationPicker({
|
|
|
671
704
|
aria-expanded={isExpanded}
|
|
672
705
|
>
|
|
673
706
|
<span className="flex items-start gap-3">
|
|
674
|
-
<span className="flex h-
|
|
707
|
+
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-ph-muted text-ph-ink">
|
|
675
708
|
{item.icon ?? (
|
|
676
|
-
<Plug className="h-
|
|
709
|
+
<Plug className="h-4 w-4" aria-hidden />
|
|
677
710
|
)}
|
|
678
711
|
</span>
|
|
679
712
|
<span className="min-w-0">
|
|
@@ -696,34 +729,33 @@ export function SuiteIntegrationPicker({
|
|
|
696
729
|
</span>
|
|
697
730
|
</span>
|
|
698
731
|
</button>
|
|
699
|
-
{
|
|
732
|
+
{bridgeApps.length > 0 ? (
|
|
700
733
|
<div
|
|
701
734
|
className="mt-3 flex flex-wrap items-center gap-1.5"
|
|
702
|
-
aria-label="
|
|
735
|
+
aria-label="Bridges into ShellStack apps"
|
|
703
736
|
>
|
|
704
|
-
{
|
|
705
|
-
|
|
706
|
-
|
|
737
|
+
{bridgeApps.map((app) => {
|
|
738
|
+
const mark = BRIDGE_MARK[app];
|
|
739
|
+
if (!mark) return null;
|
|
740
|
+
return (
|
|
741
|
+
<img
|
|
707
742
|
key={app}
|
|
743
|
+
src={mark.src}
|
|
744
|
+
alt=""
|
|
745
|
+
title={`Bridge in ${mark.name}`}
|
|
746
|
+
className="h-5 w-5"
|
|
708
747
|
data-test={`${dataTest}-${item.id}-app-${app}`}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
size={16}
|
|
713
|
-
label={`Works with ${SUITE_APP_MAP[app].name}`}
|
|
714
|
-
className="rounded-[4px]"
|
|
715
|
-
/>
|
|
716
|
-
</span>
|
|
717
|
-
),
|
|
718
|
-
)}
|
|
748
|
+
/>
|
|
749
|
+
);
|
|
750
|
+
})}
|
|
719
751
|
</div>
|
|
720
752
|
) : null}
|
|
721
|
-
{
|
|
753
|
+
{schemes.length > 0 ? (
|
|
722
754
|
<div
|
|
723
755
|
className="mt-3 flex flex-wrap gap-1.5"
|
|
724
756
|
aria-label="Connection methods"
|
|
725
757
|
>
|
|
726
|
-
{
|
|
758
|
+
{schemes.map((scheme) => (
|
|
727
759
|
<span
|
|
728
760
|
key={scheme}
|
|
729
761
|
className="rounded-full border border-ph-border bg-ph-muted px-2 py-1 text-[0.6875rem] font-medium text-ph-subtle"
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { cleanup, render, waitFor } from "@testing-library/react";
|
|
1
|
+
import { cleanup, fireEvent, render, waitFor } from "@testing-library/react";
|
|
2
2
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
4
|
vi.mock("next/navigation", () => ({
|
|
5
5
|
useRouter: () => ({ push: vi.fn() }),
|
|
6
6
|
}));
|
|
7
7
|
|
|
8
|
+
import { SuiteNotificationsProvider } from "../lib/suite-notifications";
|
|
8
9
|
import { SuiteNotificationBell } from "./suite-notification-bell";
|
|
9
10
|
|
|
10
11
|
afterEach(() => {
|
|
@@ -14,21 +15,24 @@ afterEach(() => {
|
|
|
14
15
|
describe("SuiteNotificationBell", () => {
|
|
15
16
|
it("anchors the unread badge close to the bell glyph", async () => {
|
|
16
17
|
render(
|
|
17
|
-
<
|
|
18
|
+
<SuiteNotificationsProvider
|
|
18
19
|
fetchNotifications={async () => ({
|
|
19
20
|
notifications: [],
|
|
20
21
|
unread_count: 12,
|
|
21
22
|
})}
|
|
22
23
|
markRead={async () => undefined}
|
|
23
24
|
markAllRead={async () => undefined}
|
|
24
|
-
dropdownMenu={({ trigger, "aria-label": label, children }) => (
|
|
25
|
-
<div aria-label={label}>
|
|
26
|
-
{trigger}
|
|
27
|
-
{children}
|
|
28
|
-
</div>
|
|
29
|
-
)}
|
|
30
25
|
cacheKey={null}
|
|
31
|
-
|
|
26
|
+
>
|
|
27
|
+
<SuiteNotificationBell
|
|
28
|
+
dropdownMenu={({ trigger, "aria-label": label, children }) => (
|
|
29
|
+
<div aria-label={label}>
|
|
30
|
+
{trigger}
|
|
31
|
+
{children}
|
|
32
|
+
</div>
|
|
33
|
+
)}
|
|
34
|
+
/>
|
|
35
|
+
</SuiteNotificationsProvider>,
|
|
32
36
|
);
|
|
33
37
|
|
|
34
38
|
const badge = await waitFor(() => {
|
|
@@ -44,4 +48,78 @@ describe("SuiteNotificationBell", () => {
|
|
|
44
48
|
expect(badge.parentElement?.className).toContain("h-5");
|
|
45
49
|
expect(badge.parentElement?.className).toContain("w-5");
|
|
46
50
|
});
|
|
51
|
+
|
|
52
|
+
it("keeps each responsive bell dropdown open state independent", () => {
|
|
53
|
+
render(
|
|
54
|
+
<SuiteNotificationsProvider
|
|
55
|
+
fetchNotifications={async () => ({
|
|
56
|
+
notifications: [],
|
|
57
|
+
unread_count: 0,
|
|
58
|
+
})}
|
|
59
|
+
markRead={async () => undefined}
|
|
60
|
+
markAllRead={async () => undefined}
|
|
61
|
+
cacheKey={null}
|
|
62
|
+
>
|
|
63
|
+
<SuiteNotificationBell
|
|
64
|
+
dataTest="desktop-notifications"
|
|
65
|
+
dropdownMenu={({
|
|
66
|
+
trigger,
|
|
67
|
+
children,
|
|
68
|
+
open,
|
|
69
|
+
onOpenChange,
|
|
70
|
+
triggerId,
|
|
71
|
+
}) => (
|
|
72
|
+
<div>
|
|
73
|
+
<button
|
|
74
|
+
id={triggerId}
|
|
75
|
+
type="button"
|
|
76
|
+
onClick={() => onOpenChange?.(!open)}
|
|
77
|
+
>
|
|
78
|
+
{trigger}
|
|
79
|
+
</button>
|
|
80
|
+
{open ? (
|
|
81
|
+
<div data-test={`${triggerId}-panel`}>{children}</div>
|
|
82
|
+
) : null}
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
/>
|
|
86
|
+
<SuiteNotificationBell
|
|
87
|
+
dataTest="mobile-notifications"
|
|
88
|
+
dropdownMenu={({
|
|
89
|
+
trigger,
|
|
90
|
+
children,
|
|
91
|
+
open,
|
|
92
|
+
onOpenChange,
|
|
93
|
+
triggerId,
|
|
94
|
+
}) => (
|
|
95
|
+
<div>
|
|
96
|
+
<button
|
|
97
|
+
id={triggerId}
|
|
98
|
+
type="button"
|
|
99
|
+
onClick={() => onOpenChange?.(!open)}
|
|
100
|
+
>
|
|
101
|
+
{trigger}
|
|
102
|
+
</button>
|
|
103
|
+
{open ? (
|
|
104
|
+
<div data-test={`${triggerId}-panel`}>{children}</div>
|
|
105
|
+
) : null}
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
/>
|
|
109
|
+
</SuiteNotificationsProvider>,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
fireEvent.click(document.getElementById("desktop-notifications-trigger")!);
|
|
113
|
+
|
|
114
|
+
expect(
|
|
115
|
+
document.querySelector(
|
|
116
|
+
'[data-test="desktop-notifications-trigger-panel"]',
|
|
117
|
+
),
|
|
118
|
+
).not.toBeNull();
|
|
119
|
+
expect(
|
|
120
|
+
document.querySelector(
|
|
121
|
+
'[data-test="mobile-notifications-trigger-panel"]',
|
|
122
|
+
),
|
|
123
|
+
).toBeNull();
|
|
124
|
+
});
|
|
47
125
|
});
|