mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-06 19:18:22 +01:00
87aea21184
There was still some flicker when nohover took effect before the tab view actually switched. Now, we override the active tab in the tabbar when the app is switching tabs. We also override active tab behavior so that the close button is always visible while nohover is in effect. This effectively removes the flickering
683 lines
22 KiB
TypeScript
683 lines
22 KiB
TypeScript
// Copyright 2024, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
import {
|
|
getLayoutModelForTabById,
|
|
LayoutTreeActionType,
|
|
LayoutTreeInsertNodeAction,
|
|
newLayoutNode,
|
|
} from "@/layout/index";
|
|
import { getLayoutModelForStaticTab } from "@/layout/lib/layoutModelHooks";
|
|
import { getWebServerEndpoint } from "@/util/endpoints";
|
|
import { fetch } from "@/util/fetchutil";
|
|
import { getPrefixedSettings, isBlank } from "@/util/util";
|
|
import { atom, Atom, PrimitiveAtom, useAtomValue } from "jotai";
|
|
import { globalStore } from "./jotaiStore";
|
|
import { modalsModel } from "./modalmodel";
|
|
import { ClientService, ObjectService } from "./services";
|
|
import * as WOS from "./wos";
|
|
import { getFileSubject, waveEventSubscribe } from "./wps";
|
|
|
|
let PLATFORM: NodeJS.Platform = "darwin";
|
|
let atoms: GlobalAtomsType;
|
|
let globalEnvironment: "electron" | "renderer";
|
|
const blockComponentModelMap = new Map<string, BlockComponentModel>();
|
|
const Counters = new Map<string, number>();
|
|
const ConnStatusMap = new Map<string, PrimitiveAtom<ConnStatus>>();
|
|
|
|
type GlobalInitOptions = {
|
|
tabId: string;
|
|
platform: NodeJS.Platform;
|
|
windowId: string;
|
|
clientId: string;
|
|
environment: "electron" | "renderer";
|
|
};
|
|
|
|
function initGlobal(initOpts: GlobalInitOptions) {
|
|
globalEnvironment = initOpts.environment;
|
|
setPlatform(initOpts.platform);
|
|
initGlobalAtoms(initOpts);
|
|
}
|
|
|
|
function setPlatform(platform: NodeJS.Platform) {
|
|
PLATFORM = platform;
|
|
}
|
|
|
|
// Used to override the tab id when switching tabs to prevent flicker in the tab bar.
|
|
const overrideStaticTabAtom = atom(null) as PrimitiveAtom<string>;
|
|
|
|
function initGlobalAtoms(initOpts: GlobalInitOptions) {
|
|
const windowIdAtom = atom(initOpts.windowId) as PrimitiveAtom<string>;
|
|
const clientIdAtom = atom(initOpts.clientId) as PrimitiveAtom<string>;
|
|
const uiContextAtom = atom((get) => {
|
|
const uiContext: UIContext = {
|
|
windowid: initOpts.windowId,
|
|
activetabid: initOpts.tabId,
|
|
};
|
|
return uiContext;
|
|
}) as Atom<UIContext>;
|
|
|
|
const isFullScreenAtom = atom(false) as PrimitiveAtom<boolean>;
|
|
try {
|
|
getApi().onFullScreenChange((isFullScreen) => {
|
|
globalStore.set(isFullScreenAtom, isFullScreen);
|
|
});
|
|
} catch (_) {
|
|
// do nothing
|
|
}
|
|
|
|
try {
|
|
getApi().onMenuItemAbout(() => {
|
|
modalsModel.pushModal("AboutModal");
|
|
});
|
|
} catch (_) {
|
|
// do nothing
|
|
}
|
|
|
|
const clientAtom: Atom<Client> = atom((get) => {
|
|
const clientId = get(clientIdAtom);
|
|
if (clientId == null) {
|
|
return null;
|
|
}
|
|
return WOS.getObjectValue(WOS.makeORef("client", clientId), get);
|
|
});
|
|
const windowDataAtom: Atom<WaveWindow> = atom((get) => {
|
|
const windowId = get(windowIdAtom);
|
|
if (windowId == null) {
|
|
return null;
|
|
}
|
|
const rtn = WOS.getObjectValue<WaveWindow>(WOS.makeORef("window", windowId), get);
|
|
return rtn;
|
|
});
|
|
const workspaceAtom: Atom<Workspace> = atom((get) => {
|
|
const windowData = get(windowDataAtom);
|
|
if (windowData == null) {
|
|
return null;
|
|
}
|
|
return WOS.getObjectValue(WOS.makeORef("workspace", windowData.workspaceid), get);
|
|
});
|
|
const fullConfigAtom = atom(null) as PrimitiveAtom<FullConfigType>;
|
|
const settingsAtom = atom((get) => {
|
|
return get(fullConfigAtom)?.settings ?? {};
|
|
}) as Atom<SettingsType>;
|
|
const tabAtom: Atom<Tab> = atom((get) => {
|
|
return WOS.getObjectValue(WOS.makeORef("tab", initOpts.tabId), get);
|
|
});
|
|
// This atom is used to determine the tab id to use for the static tab. It is set to the overrideStaticTabAtom value if it is not null, otherwise it is set to the initOpts.tabId value.
|
|
const staticTabIdAtom: Atom<string> = atom((get) => get(overrideStaticTabAtom) ?? initOpts.tabId);
|
|
const controlShiftDelayAtom = atom(false);
|
|
const updaterStatusAtom = atom<UpdaterStatus>("up-to-date") as PrimitiveAtom<UpdaterStatus>;
|
|
try {
|
|
globalStore.set(updaterStatusAtom, getApi().getUpdaterStatus());
|
|
getApi().onUpdaterStatusChange((status) => {
|
|
globalStore.set(updaterStatusAtom, status);
|
|
});
|
|
} catch (_) {
|
|
// do nothing
|
|
}
|
|
|
|
const reducedMotionSettingAtom = atom((get) => get(settingsAtom)?.["window:reducedmotion"]);
|
|
const reducedMotionSystemPreferenceAtom = atom(false);
|
|
|
|
// Composite of the prefers-reduced-motion media query and the window:reducedmotion user setting.
|
|
const prefersReducedMotionAtom = atom((get) => {
|
|
const reducedMotionSetting = get(reducedMotionSettingAtom);
|
|
const reducedMotionSystemPreference = get(reducedMotionSystemPreferenceAtom);
|
|
return reducedMotionSetting || reducedMotionSystemPreference;
|
|
});
|
|
|
|
// Set up a handler for changes to the prefers-reduced-motion media query.
|
|
if (globalThis.window != null) {
|
|
const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
globalStore.set(reducedMotionSystemPreferenceAtom, !reducedMotionQuery || reducedMotionQuery.matches);
|
|
reducedMotionQuery?.addEventListener("change", () => {
|
|
globalStore.set(reducedMotionSystemPreferenceAtom, reducedMotionQuery.matches);
|
|
});
|
|
}
|
|
|
|
const typeAheadModalAtom = atom({});
|
|
const modalOpen = atom(false);
|
|
const allConnStatusAtom = atom<ConnStatus[]>((get) => {
|
|
const connStatuses = Array.from(ConnStatusMap.values()).map((atom) => get(atom));
|
|
return connStatuses;
|
|
});
|
|
const flashErrorsAtom = atom<FlashErrorType[]>([]);
|
|
const notificationsAtom = atom<NotificationType[]>([]);
|
|
const notificationPopoverModeAtom = atom<boolean>(false);
|
|
const reinitVersion = atom(0);
|
|
atoms = {
|
|
// initialized in wave.ts (will not be null inside of application)
|
|
clientId: clientIdAtom,
|
|
uiContext: uiContextAtom,
|
|
client: clientAtom,
|
|
waveWindow: windowDataAtom,
|
|
workspace: workspaceAtom,
|
|
fullConfigAtom,
|
|
settingsAtom,
|
|
tabAtom,
|
|
staticTabId: staticTabIdAtom,
|
|
isFullScreen: isFullScreenAtom,
|
|
controlShiftDelayAtom,
|
|
updaterStatusAtom,
|
|
prefersReducedMotionAtom,
|
|
typeAheadModalAtom,
|
|
modalOpen,
|
|
allConnStatus: allConnStatusAtom,
|
|
flashErrors: flashErrorsAtom,
|
|
notifications: notificationsAtom,
|
|
notificationPopoverMode: notificationPopoverModeAtom,
|
|
reinitVersion,
|
|
};
|
|
}
|
|
|
|
function initGlobalWaveEventSubs() {
|
|
waveEventSubscribe(
|
|
{
|
|
eventType: "waveobj:update",
|
|
handler: (event) => {
|
|
// console.log("waveobj:update wave event handler", event);
|
|
const update: WaveObjUpdate = event.data;
|
|
WOS.updateWaveObject(update);
|
|
},
|
|
},
|
|
{
|
|
eventType: "config",
|
|
handler: (event) => {
|
|
// console.log("config wave event handler", event);
|
|
const fullConfig = (event.data as WatcherUpdate).fullconfig;
|
|
globalStore.set(atoms.fullConfigAtom, fullConfig);
|
|
},
|
|
},
|
|
{
|
|
eventType: "userinput",
|
|
handler: (event) => {
|
|
// console.log("userinput event handler", event);
|
|
const data: UserInputRequest = event.data;
|
|
modalsModel.pushModal("UserInputModal", { ...data });
|
|
},
|
|
},
|
|
{
|
|
eventType: "blockfile",
|
|
handler: (event) => {
|
|
// console.log("blockfile event update", event);
|
|
const fileData: WSFileEventData = event.data;
|
|
const fileSubject = getFileSubject(fileData.zoneid, fileData.filename);
|
|
if (fileSubject != null) {
|
|
fileSubject.next(fileData);
|
|
}
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
const blockCache = new Map<string, Map<string, any>>();
|
|
|
|
function useBlockCache<T>(blockId: string, name: string, makeFn: () => T): T {
|
|
let blockMap = blockCache.get(blockId);
|
|
if (blockMap == null) {
|
|
blockMap = new Map<string, any>();
|
|
blockCache.set(blockId, blockMap);
|
|
}
|
|
let value = blockMap.get(name);
|
|
if (value == null) {
|
|
value = makeFn();
|
|
blockMap.set(name, value);
|
|
}
|
|
return value as T;
|
|
}
|
|
|
|
function getBlockMetaKeyAtom<T extends keyof MetaType>(blockId: string, key: T): Atom<MetaType[T]> {
|
|
const blockCache = getSingleBlockAtomCache(blockId);
|
|
const metaAtomName = "#meta-" + key;
|
|
let metaAtom = blockCache.get(metaAtomName);
|
|
if (metaAtom != null) {
|
|
return metaAtom;
|
|
}
|
|
metaAtom = atom((get) => {
|
|
let blockAtom = WOS.getWaveObjectAtom(WOS.makeORef("block", blockId));
|
|
let blockData = get(blockAtom);
|
|
return blockData?.meta?.[key];
|
|
});
|
|
blockCache.set(metaAtomName, metaAtom);
|
|
return metaAtom;
|
|
}
|
|
|
|
function useBlockMetaKeyAtom<T extends keyof MetaType>(blockId: string, key: T): MetaType[T] {
|
|
return useAtomValue(getBlockMetaKeyAtom(blockId, key));
|
|
}
|
|
|
|
const settingsAtomCache = new Map<string, Atom<any>>();
|
|
|
|
function getOverrideConfigAtom<T extends keyof SettingsType>(blockId: string, key: T): Atom<SettingsType[T]> {
|
|
const blockCache = getSingleBlockAtomCache(blockId);
|
|
const overrideAtomName = "#settingsoverride-" + key;
|
|
let overrideAtom = blockCache.get(overrideAtomName);
|
|
if (overrideAtom != null) {
|
|
return overrideAtom;
|
|
}
|
|
overrideAtom = atom((get) => {
|
|
const blockMetaKeyAtom = getBlockMetaKeyAtom(blockId, key as any);
|
|
const metaKeyVal = get(blockMetaKeyAtom);
|
|
if (metaKeyVal != null) {
|
|
return metaKeyVal;
|
|
}
|
|
const settingsKeyAtom = getSettingsKeyAtom(key);
|
|
const settingsVal = get(settingsKeyAtom);
|
|
if (settingsVal != null) {
|
|
return settingsVal;
|
|
}
|
|
return null;
|
|
});
|
|
blockCache.set(overrideAtomName, overrideAtom);
|
|
return overrideAtom;
|
|
}
|
|
|
|
function useOverrideConfigAtom<T extends keyof SettingsType>(blockId: string, key: T): SettingsType[T] {
|
|
return useAtomValue(getOverrideConfigAtom(blockId, key));
|
|
}
|
|
|
|
function getSettingsKeyAtom<T extends keyof SettingsType>(key: T): Atom<SettingsType[T]> {
|
|
let settingsKeyAtom = settingsAtomCache.get(key) as Atom<SettingsType[T]>;
|
|
if (settingsKeyAtom == null) {
|
|
settingsKeyAtom = atom((get) => {
|
|
const settings = get(atoms.settingsAtom);
|
|
if (settings == null) {
|
|
return null;
|
|
}
|
|
return settings[key];
|
|
});
|
|
settingsAtomCache.set(key, settingsKeyAtom);
|
|
}
|
|
return settingsKeyAtom;
|
|
}
|
|
|
|
function useSettingsKeyAtom<T extends keyof SettingsType>(key: T): SettingsType[T] {
|
|
return useAtomValue(getSettingsKeyAtom(key));
|
|
}
|
|
|
|
function useSettingsPrefixAtom(prefix: string): Atom<SettingsType> {
|
|
// TODO: use a shallow equal here to make this more efficient
|
|
let settingsPrefixAtom = settingsAtomCache.get(prefix + ":") as Atom<SettingsType>;
|
|
if (settingsPrefixAtom == null) {
|
|
settingsPrefixAtom = atom((get) => {
|
|
const settings = get(atoms.settingsAtom);
|
|
if (settings == null) {
|
|
return {};
|
|
}
|
|
return getPrefixedSettings(settings, prefix);
|
|
});
|
|
settingsAtomCache.set(prefix + ":", settingsPrefixAtom);
|
|
}
|
|
return settingsPrefixAtom;
|
|
}
|
|
|
|
const blockAtomCache = new Map<string, Map<string, Atom<any>>>();
|
|
|
|
function getSingleBlockAtomCache(blockId: string): Map<string, Atom<any>> {
|
|
let blockCache = blockAtomCache.get(blockId);
|
|
if (blockCache == null) {
|
|
blockCache = new Map<string, Atom<any>>();
|
|
blockAtomCache.set(blockId, blockCache);
|
|
}
|
|
return blockCache;
|
|
}
|
|
|
|
function useBlockAtom<T>(blockId: string, name: string, makeFn: () => Atom<T>): Atom<T> {
|
|
const blockCache = getSingleBlockAtomCache(blockId);
|
|
let atom = blockCache.get(name);
|
|
if (atom == null) {
|
|
atom = makeFn();
|
|
blockCache.set(name, atom);
|
|
console.log("New BlockAtom", blockId, name);
|
|
}
|
|
return atom as Atom<T>;
|
|
}
|
|
|
|
function useBlockDataLoaded(blockId: string): boolean {
|
|
const loadedAtom = useBlockAtom<boolean>(blockId, "block-loaded", () => {
|
|
return WOS.getWaveObjectLoadingAtom(WOS.makeORef("block", blockId));
|
|
});
|
|
return useAtomValue(loadedAtom);
|
|
}
|
|
|
|
/**
|
|
* Get the preload api.
|
|
*/
|
|
function getApi(): ElectronApi {
|
|
return (window as any).api;
|
|
}
|
|
|
|
async function createBlock(blockDef: BlockDef, magnified = false, ephemeral = false): Promise<string> {
|
|
const tabId = globalStore.get(atoms.staticTabId);
|
|
const layoutModel = getLayoutModelForTabById(tabId);
|
|
const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } };
|
|
const blockId = await ObjectService.CreateBlock(blockDef, rtOpts);
|
|
if (ephemeral) {
|
|
layoutModel.newEphemeralNode(blockId);
|
|
return blockId;
|
|
}
|
|
const insertNodeAction: LayoutTreeInsertNodeAction = {
|
|
type: LayoutTreeActionType.InsertNode,
|
|
node: newLayoutNode(undefined, undefined, undefined, { blockId }),
|
|
magnified,
|
|
focused: true,
|
|
};
|
|
layoutModel.treeReducer(insertNodeAction);
|
|
return blockId;
|
|
}
|
|
|
|
// when file is not found, returns {data: null, fileInfo: null}
|
|
async function fetchWaveFile(
|
|
zoneId: string,
|
|
fileName: string,
|
|
offset?: number
|
|
): Promise<{ data: Uint8Array; fileInfo: WaveFile }> {
|
|
const usp = new URLSearchParams();
|
|
usp.set("zoneid", zoneId);
|
|
usp.set("name", fileName);
|
|
if (offset != null) {
|
|
usp.set("offset", offset.toString());
|
|
}
|
|
const resp = await fetch(getWebServerEndpoint() + "/wave/file?" + usp.toString());
|
|
if (!resp.ok) {
|
|
if (resp.status === 404) {
|
|
return { data: null, fileInfo: null };
|
|
}
|
|
throw new Error("error getting wave file: " + resp.statusText);
|
|
}
|
|
if (resp.status == 204) {
|
|
return { data: null, fileInfo: null };
|
|
}
|
|
const fileInfo64 = resp.headers.get("X-ZoneFileInfo");
|
|
if (fileInfo64 == null) {
|
|
throw new Error(`missing zone file info for ${zoneId}:${fileName}`);
|
|
}
|
|
const fileInfo = JSON.parse(atob(fileInfo64));
|
|
const data = await resp.arrayBuffer();
|
|
return { data: new Uint8Array(data), fileInfo };
|
|
}
|
|
|
|
function setNodeFocus(nodeId: string) {
|
|
const layoutModel = getLayoutModelForStaticTab();
|
|
layoutModel.focusNode(nodeId);
|
|
}
|
|
|
|
const objectIdWeakMap = new WeakMap();
|
|
let objectIdCounter = 0;
|
|
function getObjectId(obj: any): number {
|
|
if (!objectIdWeakMap.has(obj)) {
|
|
objectIdWeakMap.set(obj, objectIdCounter++);
|
|
}
|
|
return objectIdWeakMap.get(obj);
|
|
}
|
|
|
|
let cachedIsDev: boolean = null;
|
|
|
|
function isDev() {
|
|
if (cachedIsDev == null) {
|
|
cachedIsDev = getApi().getIsDev();
|
|
}
|
|
return cachedIsDev;
|
|
}
|
|
|
|
let cachedUserName: string = null;
|
|
|
|
function getUserName(): string {
|
|
if (cachedUserName == null) {
|
|
cachedUserName = getApi().getUserName();
|
|
}
|
|
return cachedUserName;
|
|
}
|
|
|
|
let cachedHostName: string = null;
|
|
|
|
function getHostName(): string {
|
|
if (cachedHostName == null) {
|
|
cachedHostName = getApi().getHostName();
|
|
}
|
|
return cachedHostName;
|
|
}
|
|
|
|
/**
|
|
* Open a link in a new window, or in a new web widget. The user can set all links to open in a new web widget using the `web:openlinksinternally` setting.
|
|
* @param uri The link to open.
|
|
* @param forceOpenInternally Force the link to open in a new web widget.
|
|
*/
|
|
async function openLink(uri: string, forceOpenInternally = false) {
|
|
if (forceOpenInternally || globalStore.get(atoms.settingsAtom)?.["web:openlinksinternally"]) {
|
|
const blockDef: BlockDef = {
|
|
meta: {
|
|
view: "web",
|
|
url: uri,
|
|
},
|
|
};
|
|
await createBlock(blockDef);
|
|
} else {
|
|
getApi().openExternal(uri);
|
|
}
|
|
}
|
|
|
|
function registerBlockComponentModel(blockId: string, bcm: BlockComponentModel) {
|
|
blockComponentModelMap.set(blockId, bcm);
|
|
}
|
|
|
|
function unregisterBlockComponentModel(blockId: string) {
|
|
blockComponentModelMap.delete(blockId);
|
|
}
|
|
|
|
function getBlockComponentModel(blockId: string): BlockComponentModel {
|
|
return blockComponentModelMap.get(blockId);
|
|
}
|
|
|
|
function getFocusedBlockId(): string {
|
|
const layoutModel = getLayoutModelForStaticTab();
|
|
const focusedLayoutNode = globalStore.get(layoutModel.focusedNode);
|
|
return focusedLayoutNode?.data?.blockId;
|
|
}
|
|
|
|
// pass null to refocus the currently focused block
|
|
function refocusNode(blockId: string) {
|
|
if (blockId == null) {
|
|
blockId = getFocusedBlockId();
|
|
if (blockId == null) {
|
|
return;
|
|
}
|
|
}
|
|
const layoutModel = getLayoutModelForStaticTab();
|
|
const layoutNodeId = layoutModel.getNodeByBlockId(blockId);
|
|
if (layoutNodeId?.id == null) {
|
|
return;
|
|
}
|
|
layoutModel.focusNode(layoutNodeId.id);
|
|
const bcm = getBlockComponentModel(blockId);
|
|
const ok = bcm?.viewModel?.giveFocus?.();
|
|
if (!ok) {
|
|
const inputElem = document.getElementById(`${blockId}-dummy-focus`);
|
|
inputElem?.focus();
|
|
}
|
|
}
|
|
|
|
function countersClear() {
|
|
Counters.clear();
|
|
}
|
|
|
|
function counterInc(name: string, incAmt: number = 1) {
|
|
let count = Counters.get(name) ?? 0;
|
|
count += incAmt;
|
|
Counters.set(name, count);
|
|
}
|
|
|
|
function countersPrint() {
|
|
let outStr = "";
|
|
for (const [name, count] of Counters.entries()) {
|
|
outStr += `${name}: ${count}\n`;
|
|
}
|
|
console.log(outStr);
|
|
}
|
|
|
|
async function loadConnStatus() {
|
|
const connStatusArr = await ClientService.GetAllConnStatus();
|
|
if (connStatusArr == null) {
|
|
return;
|
|
}
|
|
for (const connStatus of connStatusArr) {
|
|
const curAtom = getConnStatusAtom(connStatus.connection);
|
|
globalStore.set(curAtom, connStatus);
|
|
}
|
|
}
|
|
|
|
function subscribeToConnEvents() {
|
|
waveEventSubscribe({
|
|
eventType: "connchange",
|
|
handler: (event: WaveEvent) => {
|
|
try {
|
|
const connStatus = event.data as ConnStatus;
|
|
if (connStatus == null || isBlank(connStatus.connection)) {
|
|
return;
|
|
}
|
|
console.log("connstatus update", connStatus);
|
|
let curAtom = getConnStatusAtom(connStatus.connection);
|
|
globalStore.set(curAtom, connStatus);
|
|
} catch (e) {
|
|
console.log("connchange error", e);
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
function getConnStatusAtom(conn: string): PrimitiveAtom<ConnStatus> {
|
|
let rtn = ConnStatusMap.get(conn);
|
|
if (rtn == null) {
|
|
if (isBlank(conn)) {
|
|
// create a fake "local" status atom that's always connected
|
|
const connStatus: ConnStatus = {
|
|
connection: conn,
|
|
connected: true,
|
|
error: null,
|
|
status: "connected",
|
|
hasconnected: true,
|
|
activeconnnum: 0,
|
|
wshenabled: false,
|
|
};
|
|
rtn = atom(connStatus);
|
|
} else {
|
|
const connStatus: ConnStatus = {
|
|
connection: conn,
|
|
connected: false,
|
|
error: null,
|
|
status: "disconnected",
|
|
hasconnected: false,
|
|
activeconnnum: 0,
|
|
wshenabled: false,
|
|
};
|
|
rtn = atom(connStatus);
|
|
}
|
|
ConnStatusMap.set(conn, rtn);
|
|
}
|
|
return rtn;
|
|
}
|
|
|
|
function pushFlashError(ferr: FlashErrorType) {
|
|
if (ferr.expiration == null) {
|
|
ferr.expiration = Date.now() + 5000;
|
|
}
|
|
ferr.id = crypto.randomUUID();
|
|
globalStore.set(atoms.flashErrors, (prev) => {
|
|
return [...prev, ferr];
|
|
});
|
|
}
|
|
|
|
function addOrUpdateNotification(notif: NotificationType) {
|
|
globalStore.set(atoms.notifications, (prevNotifications) => {
|
|
// Remove any existing notification with the same ID
|
|
const notificationsWithoutThisId = prevNotifications.filter((n) => n.id !== notif.id);
|
|
// Add the new notification
|
|
return [...notificationsWithoutThisId, notif];
|
|
});
|
|
}
|
|
|
|
function pushNotification(notif: NotificationType) {
|
|
if (!notif.id && notif.persistent) {
|
|
return;
|
|
}
|
|
notif.id = notif.id ?? crypto.randomUUID();
|
|
addOrUpdateNotification(notif);
|
|
}
|
|
|
|
function removeNotificationById(id: string) {
|
|
globalStore.set(atoms.notifications, (prev) => {
|
|
return prev.filter((notif) => notif.id !== id);
|
|
});
|
|
}
|
|
|
|
function removeFlashError(id: string) {
|
|
globalStore.set(atoms.flashErrors, (prev) => {
|
|
return prev.filter((ferr) => ferr.id !== id);
|
|
});
|
|
}
|
|
|
|
function removeNotification(id: string) {
|
|
globalStore.set(atoms.notifications, (prev) => {
|
|
return prev.filter((notif) => notif.id !== id);
|
|
});
|
|
}
|
|
|
|
function createTab() {
|
|
getApi().createTab();
|
|
}
|
|
|
|
function setActiveTab(tabId: string) {
|
|
// We use this hack to prevent a flicker of the previously-hovered tab when this view was last active. This class is set in setActiveTab in global.ts. See tab.scss for where this class is used.
|
|
// Also overrides the staticTabAtom to the new tab id so that the active tab is set correctly.
|
|
globalStore.set(overrideStaticTabAtom, tabId);
|
|
document.body.classList.add("nohover");
|
|
getApi().setActiveTab(tabId);
|
|
}
|
|
|
|
export {
|
|
atoms,
|
|
counterInc,
|
|
countersClear,
|
|
countersPrint,
|
|
createBlock,
|
|
createTab,
|
|
fetchWaveFile,
|
|
getApi,
|
|
getBlockComponentModel,
|
|
getBlockMetaKeyAtom,
|
|
getConnStatusAtom,
|
|
getHostName,
|
|
getObjectId,
|
|
getOverrideConfigAtom,
|
|
getSettingsKeyAtom,
|
|
getUserName,
|
|
globalStore,
|
|
initGlobal,
|
|
initGlobalWaveEventSubs,
|
|
isDev,
|
|
loadConnStatus,
|
|
openLink,
|
|
overrideStaticTabAtom,
|
|
PLATFORM,
|
|
pushFlashError,
|
|
pushNotification,
|
|
refocusNode,
|
|
registerBlockComponentModel,
|
|
removeFlashError,
|
|
removeNotification,
|
|
removeNotificationById,
|
|
setActiveTab,
|
|
setNodeFocus,
|
|
setPlatform,
|
|
subscribeToConnEvents,
|
|
unregisterBlockComponentModel,
|
|
useBlockAtom,
|
|
useBlockCache,
|
|
useBlockDataLoaded,
|
|
useBlockMetaKeyAtom,
|
|
useOverrideConfigAtom,
|
|
useSettingsKeyAtom,
|
|
useSettingsPrefixAtom,
|
|
WOS,
|
|
};
|