Skip to content

Commit 90f4e22

Browse files
committed
mock Tab component directly (instead of TabV), and get badge mocking working
1 parent 41f419a commit 90f4e22

5 files changed

Lines changed: 128 additions & 44 deletions

File tree

frontend/app/store/badge.ts

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,42 @@
33

44
import { RpcApi } from "@/app/store/wshclientapi";
55
import { TabRpcClient } from "@/app/store/wshrpcutil";
6+
import { WaveEnv, WaveEnvSubset } from "@/app/waveenv/waveenv";
67
import { fireAndForget, NullAtom } from "@/util/util";
78
import { atom, Atom, PrimitiveAtom } from "jotai";
89
import { v7 as uuidv7, version as uuidVersion } from "uuid";
910
import { globalStore } from "./jotaiStore";
1011
import * as WOS from "./wos";
1112
import { waveEventSubscribeSingle } from "./wps";
1213

14+
export type BadgeEnv = WaveEnvSubset<{
15+
rpc: {
16+
EventPublishCommand: WaveEnv["rpc"]["EventPublishCommand"];
17+
};
18+
}>;
19+
20+
export type LoadBadgesEnv = WaveEnvSubset<{
21+
rpc: {
22+
GetAllBadgesCommand: WaveEnv["rpc"]["GetAllBadgesCommand"];
23+
};
24+
}>;
25+
26+
export type TabBadgesEnv = WaveEnvSubset<{
27+
wos: WaveEnv["wos"];
28+
}>;
29+
1330
const BadgeMap = new Map<string, PrimitiveAtom<Badge>>();
1431
const TabBadgeAtomCache = new Map<string, Atom<Badge[]>>();
1532

16-
function clearBadgeInternal(oref: string) {
33+
function publishBadgeEvent(eventData: WaveEvent, env?: BadgeEnv) {
34+
if (env != null) {
35+
fireAndForget(() => env.rpc.EventPublishCommand(TabRpcClient, eventData));
36+
} else {
37+
fireAndForget(() => RpcApi.EventPublishCommand(TabRpcClient, eventData));
38+
}
39+
}
40+
41+
function clearBadgeInternal(oref: string, env?: BadgeEnv) {
1742
const eventData: WaveEvent = {
1843
event: "badge",
1944
scopes: [oref],
@@ -22,28 +47,28 @@ function clearBadgeInternal(oref: string) {
2247
clear: true,
2348
} as BadgeEvent,
2449
};
25-
fireAndForget(() => RpcApi.EventPublishCommand(TabRpcClient, eventData));
50+
publishBadgeEvent(eventData, env);
2651
}
2752

28-
function clearBadgesForBlockOnFocus(blockId: string) {
53+
function clearBadgesForBlockOnFocus(blockId: string, env?: BadgeEnv) {
2954
const oref = WOS.makeORef("block", blockId);
3055
const badgeAtom = BadgeMap.get(oref);
3156
const badge = badgeAtom != null ? globalStore.get(badgeAtom) : null;
3257
if (badge != null && !badge.pidlinked) {
33-
clearBadgeInternal(oref);
58+
clearBadgeInternal(oref, env);
3459
}
3560
}
3661

37-
function clearBadgesForTabOnFocus(tabId: string) {
62+
function clearBadgesForTabOnFocus(tabId: string, env?: BadgeEnv) {
3863
const oref = WOS.makeORef("tab", tabId);
3964
const badgeAtom = BadgeMap.get(oref);
4065
const badge = badgeAtom != null ? globalStore.get(badgeAtom) : null;
4166
if (badge != null && !badge.pidlinked) {
42-
clearBadgeInternal(oref);
67+
clearBadgeInternal(oref, env);
4368
}
4469
}
4570

46-
function clearAllBadges() {
71+
function clearAllBadges(env?: BadgeEnv) {
4772
const eventData: WaveEvent = {
4873
event: "badge",
4974
scopes: [],
@@ -52,18 +77,18 @@ function clearAllBadges() {
5277
clearall: true,
5378
} as BadgeEvent,
5479
};
55-
fireAndForget(() => RpcApi.EventPublishCommand(TabRpcClient, eventData));
80+
publishBadgeEvent(eventData, env);
5681
}
5782

58-
function clearBadgesForTab(tabId: string) {
83+
function clearBadgesForTab(tabId: string, env?: BadgeEnv) {
5984
const tabAtom = WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", tabId));
6085
const tab = globalStore.get(tabAtom);
6186
const blockIds = (tab as Tab)?.blockids ?? [];
6287
for (const blockId of blockIds) {
6388
const oref = WOS.makeORef("block", blockId);
6489
const badgeAtom = BadgeMap.get(oref);
6590
if (badgeAtom != null && globalStore.get(badgeAtom) != null) {
66-
clearBadgeInternal(oref);
91+
clearBadgeInternal(oref, env);
6792
}
6893
}
6994
}
@@ -88,7 +113,7 @@ function getBlockBadgeAtom(blockId: string): Atom<Badge> {
88113
return getBadgeAtom(oref);
89114
}
90115

91-
function getTabBadgeAtom(tabId: string): Atom<Badge[]> {
116+
function getTabBadgeAtom(tabId: string, env?: TabBadgesEnv): Atom<Badge[]> {
92117
if (tabId == null) {
93118
return NullAtom as Atom<Badge[]>;
94119
}
@@ -98,7 +123,8 @@ function getTabBadgeAtom(tabId: string): Atom<Badge[]> {
98123
}
99124
const tabOref = WOS.makeORef("tab", tabId);
100125
const tabBadgeAtom = getBadgeAtom(tabOref);
101-
const tabAtom = atom((get) => WOS.getObjectValue<Tab>(tabOref, get));
126+
const tabAtom =
127+
env != null ? env.wos.getWaveObjectAtom<Tab>(tabOref) : WOS.getWaveObjectAtom<Tab>(tabOref);
102128
rtn = atom((get) => {
103129
const tab = get(tabAtom);
104130
const blockIds = tab?.blockids ?? [];
@@ -119,8 +145,9 @@ function getTabBadgeAtom(tabId: string): Atom<Badge[]> {
119145
return rtn;
120146
}
121147

122-
async function loadBadges() {
123-
const badges = await RpcApi.GetAllBadgesCommand(TabRpcClient);
148+
async function loadBadges(env?: LoadBadgesEnv) {
149+
const rpc = env != null ? env.rpc : RpcApi;
150+
const badges = await rpc.GetAllBadgesCommand(TabRpcClient);
124151
if (badges == null) {
125152
return;
126153
}
@@ -133,7 +160,7 @@ async function loadBadges() {
133160
}
134161
}
135162

136-
function setBadge(blockId: string, badge: Omit<Badge, "badgeid"> & { badgeid?: string }) {
163+
function setBadge(blockId: string, badge: Omit<Badge, "badgeid"> & { badgeid?: string }, env?: BadgeEnv) {
137164
if (!badge.badgeid) {
138165
badge = { ...badge, badgeid: uuidv7() };
139166
} else if (uuidVersion(badge.badgeid) !== 7) {
@@ -148,10 +175,10 @@ function setBadge(blockId: string, badge: Omit<Badge, "badgeid"> & { badgeid?: s
148175
badge: badge,
149176
} as BadgeEvent,
150177
};
151-
fireAndForget(() => RpcApi.EventPublishCommand(TabRpcClient, eventData));
178+
publishBadgeEvent(eventData, env);
152179
}
153180

154-
function clearBadgeById(blockId: string, badgeId: string) {
181+
function clearBadgeById(blockId: string, badgeId: string, env?: BadgeEnv) {
155182
const oref = WOS.makeORef("block", blockId);
156183
const eventData: WaveEvent = {
157184
event: "badge",
@@ -161,7 +188,7 @@ function clearBadgeById(blockId: string, badgeId: string) {
161188
clearbyid: badgeId,
162189
} as BadgeEvent,
163190
};
164-
fireAndForget(() => RpcApi.EventPublishCommand(TabRpcClient, eventData));
191+
publishBadgeEvent(eventData, env);
165192
}
166193

167194
function setupBadgesSubscription() {

frontend/app/tab/tab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ const TabInner = forwardRef<HTMLDivElement, TabProps>((props, ref) => {
350350
const { id, active, showDivider, isDragging, tabWidth, isNew, onLoaded, onSelect, onClose, onDragStart } = props;
351351
const env = useWaveEnv<TabEnv>();
352352
const [tabData, _] = env.wos.useWaveObjectValue<Tab>(makeORef("tab", id));
353-
const badges = useAtomValue(getTabBadgeAtom(id));
353+
const badges = useAtomValue(getTabBadgeAtom(id, env));
354354

355355
const rawFlagColor = tabData?.meta?.["tab:flagcolor"];
356356
let flagColor: string | null = null;

frontend/preview/previews/tabbar.preview.tsx

Lines changed: 78 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
import WorkspaceSVG from "@/app/asset/workspace.svg";
5-
import { Tooltip } from "@/app/element/tooltip";
65
import { IconButton } from "@/app/element/iconbutton";
6+
import { Tooltip } from "@/app/element/tooltip";
7+
import { loadBadges, LoadBadgesEnv } from "@/app/store/badge";
78
import { getAtoms } from "@/app/store/global-atoms";
8-
import { TabV } from "@/app/tab/tab";
9+
import { Tab } from "@/app/tab/tab";
910
import { ConfigErrorIcon, WaveAIButton } from "@/app/tab/tabbar";
1011
import { TabBarEnv } from "@/app/tab/tabbarenv";
1112
import { UpdateStatusBanner } from "@/app/tab/updatebanner";
12-
import { useWaveEnv } from "@/app/waveenv/waveenv";
13+
import { useWaveEnv, WaveEnvContext } from "@/app/waveenv/waveenv";
14+
import { applyMockEnvOverrides } from "@/preview/mock/mockwaveenv";
1315
import { useAtom } from "jotai";
1416
import { CSSProperties, useEffect, useMemo, useRef, useState } from "react";
1517

@@ -20,26 +22,61 @@ type PreviewTabEntry = {
2022
flagColor?: string | null;
2123
};
2224

25+
function badgeBlockId(tabId: string, badgeId: string): string {
26+
return `${tabId}-badge-${badgeId}`;
27+
}
28+
29+
function makeTabWaveObj(tab: PreviewTabEntry): Tab {
30+
const blockids = (tab.badges ?? []).map((b) => badgeBlockId(tab.tabId, b.badgeid));
31+
return {
32+
otype: "tab",
33+
oid: tab.tabId,
34+
version: 1,
35+
name: tab.tabName,
36+
blockids,
37+
meta: tab.flagColor ? { "tab:flagcolor": tab.flagColor } : {},
38+
} as Tab;
39+
}
40+
41+
function makeMockBadgeEvents(): BadgeEvent[] {
42+
const events: BadgeEvent[] = [];
43+
for (const tab of InitialTabs) {
44+
for (const badge of tab.badges ?? []) {
45+
events.push({ oref: `block:${badgeBlockId(tab.tabId, badge.badgeid)}`, badge });
46+
}
47+
}
48+
return events;
49+
}
50+
2351
const TabDefaultWidth = 130;
2452
const TabMinWidth = 100;
2553
const TabHeight = 26;
2654
const MockWorkspaceSwitcherWidth = 42;
2755
const MockAddTabButtonWidth = 44;
2856
const MockConfigErrors: ConfigError[] = [
29-
{ file: "~/.waveterm/config.json", err: "unknown preset \"bg@aurora\"" },
57+
{ file: "~/.waveterm/config.json", err: 'unknown preset "bg@aurora"' },
3058
{ file: "~/.waveterm/settings.json", err: "invalid color for tab theme" },
3159
];
3260
const InitialTabs: PreviewTabEntry[] = [
3361
{ tabId: "preview-tab-1", tabName: "Terminal" },
3462
{
3563
tabId: "preview-tab-2",
3664
tabName: "Build Logs",
37-
badges: [{ badgeid: "01958000-0000-7000-0000-000000000001", icon: "triangle-exclamation", color: "#f59e0b", priority: 2 }],
65+
badges: [
66+
{
67+
badgeid: "01958000-0000-7000-0000-000000000001",
68+
icon: "triangle-exclamation",
69+
color: "#f59e0b",
70+
priority: 2,
71+
},
72+
],
3873
},
3974
{
4075
tabId: "preview-tab-3",
4176
tabName: "Deploy",
42-
badges: [{ badgeid: "01958000-0000-7000-0000-000000000002", icon: "circle-check", color: "#4ade80", priority: 3 }],
77+
badges: [
78+
{ badgeid: "01958000-0000-7000-0000-000000000002", icon: "circle-check", color: "#4ade80", priority: 3 },
79+
],
4380
flagColor: "#429dff",
4481
},
4582
{
@@ -74,7 +111,13 @@ function getWindowDragWidths(platform: NodeJS.Platform, isFullScreen: boolean, z
74111

75112
function MockWorkspaceSwitcher({ divRef }: { divRef: React.RefObject<HTMLDivElement> }) {
76113
return (
77-
<Tooltip content="Workspace Switcher" placement="bottom" hideOnClick divRef={divRef} divClassName="flex items-center">
114+
<Tooltip
115+
content="Workspace Switcher"
116+
placement="bottom"
117+
hideOnClick
118+
divRef={divRef}
119+
divClassName="flex items-center"
120+
>
78121
<div
79122
className="mb-1 mr-1 flex h-[22px] w-[28px] items-center justify-center rounded-md bg-hover text-secondary"
80123
style={{ WebkitAppRegion: "no-drag" } as CSSProperties}
@@ -91,14 +134,12 @@ function MockTabStrip({
91134
availableWidth,
92135
onSelectTab,
93136
onCloseTab,
94-
onRenameTab,
95137
}: {
96138
tabs: PreviewTabEntry[];
97139
activeTabId: string;
98140
availableWidth: number;
99141
onSelectTab: (tabId: string) => void;
100142
onCloseTab: (tabId: string) => void;
101-
onRenameTab: (tabId: string, newName: string) => void;
102143
}) {
103144
const tabRefs = useRef<Record<string, HTMLDivElement | null>>({});
104145
const tabWidth = useMemo(() => {
@@ -128,25 +169,21 @@ function MockTabStrip({
128169
const isActive = tab.tabId === activeTabId;
129170
const showDivider = index !== 0 && !isActive && index !== activeIndex + 1;
130171
return (
131-
<TabV
172+
<Tab
132173
key={tab.tabId}
133174
ref={(el) => {
134175
tabRefs.current[tab.tabId] = el;
135176
}}
136-
tabId={tab.tabId}
137-
tabName={tab.tabName}
177+
id={tab.tabId}
138178
active={isActive}
139179
showDivider={showDivider}
140180
isDragging={false}
141181
tabWidth={tabWidth}
142182
isNew={false}
143-
badges={tab.badges ?? null}
144-
flagColor={tab.flagColor ?? null}
145-
onClick={() => onSelectTab(tab.tabId)}
183+
onSelect={() => onSelectTab(tab.tabId)}
146184
onClose={() => onCloseTab(tab.tabId)}
147185
onDragStart={() => {}}
148-
onContextMenu={() => {}}
149-
onRename={(newName) => onRenameTab(tab.tabId, newName)}
186+
onLoaded={() => {}}
150187
/>
151188
);
152189
})}
@@ -156,7 +193,26 @@ function MockTabStrip({
156193
}
157194

158195
export function TabBarPreview() {
196+
const baseEnv = useWaveEnv();
197+
const tabEnv = useMemo(() => {
198+
const mockWaveObjs = Object.fromEntries(InitialTabs.map((tab) => [`tab:${tab.tabId}`, makeTabWaveObj(tab)]));
199+
return applyMockEnvOverrides(baseEnv, {
200+
mockWaveObjs,
201+
rpc: {
202+
GetAllBadgesCommand: () => Promise.resolve(makeMockBadgeEvents()),
203+
},
204+
});
205+
}, []);
206+
return (
207+
<WaveEnvContext.Provider value={tabEnv}>
208+
<TabBarPreviewInner />
209+
</WaveEnvContext.Provider>
210+
);
211+
}
212+
213+
function TabBarPreviewInner() {
159214
const env = useWaveEnv<TabBarEnv>();
215+
const loadBadgesEnv = useWaveEnv<LoadBadgesEnv>();
160216
const [tabs, setTabs] = useState<PreviewTabEntry[]>(InitialTabs);
161217
const [activeTabId, setActiveTabId] = useState<string>(InitialTabs[1].tabId);
162218
const [frameWidth, setFrameWidth] = useState(1180);
@@ -173,6 +229,10 @@ export function TabBarPreview() {
173229
const updateStatusBannerRef = useRef<HTMLButtonElement>(null);
174230
const configErrorButtonRef = useRef<HTMLElement>(null);
175231

232+
useEffect(() => {
233+
loadBadges(loadBadgesEnv);
234+
}, []);
235+
176236
useEffect(() => {
177237
setFullConfig((prev) => ({
178238
...(prev ?? ({} as FullConfigType)),
@@ -334,11 +394,6 @@ export function TabBarPreview() {
334394
return nextTabs;
335395
});
336396
}}
337-
onRenameTab={(tabId, newName) => {
338-
setTabs((prevTabs) =>
339-
prevTabs.map((tab) => (tab.tabId === tabId ? { ...tab, tabName: newName } : tab))
340-
);
341-
}}
342397
/>
343398
</div>
344399
<IconButton
@@ -372,3 +427,4 @@ export function TabBarPreview() {
372427
</div>
373428
);
374429
}
430+
TabBarPreviewInner.displayName = "TabBarPreviewInner";

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"@/store/*": ["frontend/app/store/*"],
2323
"@/view/*": ["frontend/app/view/*"],
2424
"@/element/*": ["frontend/app/element/*"],
25-
"@/shadcn/*": ["frontend/app/shadcn/*"]
25+
"@/shadcn/*": ["frontend/app/shadcn/*"],
26+
"@/preview/*": ["frontend/preview/*"]
2627
},
2728
"lib": ["dom", "dom.iterable", "es6"],
2829
"allowJs": true,

0 commit comments

Comments
 (0)