Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 54 additions & 46 deletions frontend/app/block/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,8 @@ import { ErrorBoundary } from "@/element/errorboundary";
import { CenteredDiv } from "@/element/quickelems";
import { useDebouncedNodeInnerRect } from "@/layout/index";
import { counterInc } from "@/store/counters";
import {
atoms,
getBlockComponentModel,
getSettingsKeyAtom,
registerBlockComponentModel,
unregisterBlockComponentModel,
} from "@/store/global";
import { getWaveObjectAtom, makeORef, useWaveObjectValue } from "@/store/wos";
import { getBlockComponentModel, registerBlockComponentModel, unregisterBlockComponentModel } from "@/store/global";
import { makeORef } from "@/store/wos";
import { focusedBlockId, getElemAsStr } from "@/util/focusutil";
import { isBlank, useAtomValueSafe } from "@/util/util";
import { HelpViewModel } from "@/view/helpview/helpview";
Expand All @@ -42,6 +36,7 @@ import { memo, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRe
import { QuickTipsViewModel } from "../view/quicktipsview/quicktipsview";
import { WaveConfigViewModel } from "../view/waveconfig/waveconfig-model";
import "./block.scss";
import { BlockEnv } from "./blockenv";
import { BlockFrame } from "./blockframe";
import { blockViewToIcon, blockViewToName } from "./blockutil";

Expand Down Expand Up @@ -71,7 +66,7 @@ function makeViewModel(
if (ctor != null) {
return new ctor({ blockId, nodeModel, tabModel, waveEnv });
}
return makeDefaultViewModel(blockId, blockView);
return makeDefaultViewModel(blockView);
}

function getViewElem(
Expand All @@ -91,18 +86,11 @@ function getViewElem(
return <VC key={blockId} blockId={blockId} blockRef={blockRef} contentRef={contentRef} model={viewModel} />;
}

function makeDefaultViewModel(blockId: string, viewType: string): ViewModel {
const blockDataAtom = getWaveObjectAtom<Block>(makeORef("block", blockId));
function makeDefaultViewModel(viewType: string): ViewModel {
const viewModel: ViewModel = {
viewType: viewType,
viewIcon: atom((get) => {
const blockData = get(blockDataAtom);
return blockViewToIcon(blockData?.meta?.view);
}),
viewName: atom((get) => {
const blockData = get(blockDataAtom);
return blockViewToName(blockData?.meta?.view);
}),
viewIcon: atom(blockViewToIcon(viewType)),
viewName: atom(blockViewToName(viewType)),
preIconButton: atom(null),
endIconButtons: atom(null),
viewComponent: null,
Expand All @@ -111,8 +99,9 @@ function makeDefaultViewModel(blockId: string, viewType: string): ViewModel {
}

const BlockPreview = memo(({ nodeModel, viewModel }: FullBlockProps) => {
const [blockData] = useWaveObjectValue<Block>(makeORef("block", nodeModel.blockId));
if (!blockData) {
const waveEnv = useWaveEnv<BlockEnv>();
const blockIsNull = useAtomValue(waveEnv.wos.isWaveObjectNullAtom(makeORef("block", nodeModel.blockId)));
if (blockIsNull) {
return null;
}
return (
Expand All @@ -127,15 +116,17 @@ const BlockPreview = memo(({ nodeModel, viewModel }: FullBlockProps) => {
});

const BlockSubBlock = memo(({ nodeModel, viewModel }: FullSubBlockProps) => {
const [blockData] = useWaveObjectValue<Block>(makeORef("block", nodeModel.blockId));
const waveEnv = useWaveEnv<BlockEnv>();
const blockIsNull = useAtomValue(waveEnv.wos.isWaveObjectNullAtom(makeORef("block", nodeModel.blockId)));
const blockView = useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "view")) ?? "";
const blockRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const viewElem = useMemo(
() => getViewElem(nodeModel.blockId, blockRef, contentRef, blockData?.meta?.view, viewModel),
[nodeModel.blockId, blockData?.meta?.view, viewModel]
() => getViewElem(nodeModel.blockId, blockRef, contentRef, blockView, viewModel),
[nodeModel.blockId, blockView, viewModel]
);
const noPadding = useAtomValueSafe(viewModel.noPadding);
if (!blockData) {
if (blockIsNull) {
return null;
}
return (
Expand All @@ -149,18 +140,19 @@ const BlockSubBlock = memo(({ nodeModel, viewModel }: FullSubBlockProps) => {

const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
counterInc("render-BlockFull");
const waveEnv = useWaveEnv<BlockEnv>();
const focusElemRef = useRef<HTMLInputElement>(null);
const blockRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const [blockClicked, setBlockClicked] = useState(false);
const [blockData] = useWaveObjectValue<Block>(makeORef("block", nodeModel.blockId));
const blockView = useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "view")) ?? "";
const isFocused = useAtomValue(nodeModel.isFocused);
const disablePointerEvents = useAtomValue(nodeModel.disablePointerEvents);
const isResizing = useAtomValue(nodeModel.isResizing);
const isMagnified = useAtomValue(nodeModel.isMagnified);
const anyMagnified = useAtomValue(nodeModel.anyMagnified);
const modalOpen = useAtomValue(atoms.modalOpen);
const focusFollowsCursorMode = useAtomValue(getSettingsKeyAtom("app:focusfollowscursor")) ?? "off";
const modalOpen = useAtomValue(waveEnv.atoms.modalOpen);
const focusFollowsCursorMode = useAtomValue(waveEnv.getSettingsKeyAtom("app:focusfollowscursor")) ?? "off";
const innerRect = useDebouncedNodeInnerRect(nodeModel);
const noPadding = useAtomValueSafe(viewModel.noPadding);

Expand Down Expand Up @@ -213,8 +205,8 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
}, [innerRect, disablePointerEvents, blockContentOffset]);

const viewElem = useMemo(
() => getViewElem(nodeModel.blockId, blockRef, contentRef, blockData?.meta?.view, viewModel),
[nodeModel.blockId, blockData?.meta?.view, viewModel]
() => getViewElem(nodeModel.blockId, blockRef, contentRef, blockView, viewModel),
[nodeModel.blockId, blockView, viewModel]
);

const handleChildFocus = useCallback(
Expand All @@ -240,7 +232,7 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
(event: React.PointerEvent<HTMLDivElement>) => {
const focusFollowsCursorEnabled =
focusFollowsCursorMode === "on" ||
(focusFollowsCursorMode === "term" && blockData?.meta?.view === "term");
(focusFollowsCursorMode === "term" && blockView === "term");
if (!focusFollowsCursorEnabled || event.pointerType === "touch" || event.buttons > 0) {
return;
}
Expand All @@ -257,7 +249,7 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
},
[
focusFollowsCursorMode,
blockData?.meta?.view,
blockView,
modalOpen,
disablePointerEvents,
isResizing,
Expand Down Expand Up @@ -311,16 +303,16 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
);
});

const Block = memo((props: BlockProps) => {
const BlockInner = memo((props: BlockProps & { viewType: string }) => {
counterInc("render-Block");
counterInc("render-Block-" + props.nodeModel?.blockId?.substring(0, 8));
const tabModel = useTabModel();
const waveEnv = useWaveEnv();
const [blockData, loading] = useWaveObjectValue<Block>(makeORef("block", props.nodeModel.blockId));
const bcm = getBlockComponentModel(props.nodeModel.blockId);
let viewModel = bcm?.viewModel;
if (viewModel == null || viewModel.viewType != blockData?.meta?.view) {
viewModel = makeViewModel(props.nodeModel.blockId, blockData?.meta?.view, props.nodeModel, tabModel, waveEnv);
if (viewModel == null) {
// viewModel gets the full waveEnv
viewModel = makeViewModel(props.nodeModel.blockId, props.viewType, props.nodeModel, tabModel, waveEnv);
registerBlockComponentModel(props.nodeModel.blockId, { viewModel });
}
useEffect(() => {
Expand All @@ -329,25 +321,33 @@ const Block = memo((props: BlockProps) => {
viewModel?.dispose?.();
};
}, []);
if (loading || isBlank(props.nodeModel.blockId) || blockData == null) {
return null;
}
if (props.preview) {
return <BlockPreview {...props} viewModel={viewModel} />;
}
return <BlockFull {...props} viewModel={viewModel} />;
});
BlockInner.displayName = "BlockInner";

const SubBlock = memo((props: SubBlockProps) => {
const Block = memo((props: BlockProps) => {
const waveEnv = useWaveEnv<BlockEnv>();
const isNull = useAtomValue(waveEnv.wos.isWaveObjectNullAtom(makeORef("block", props.nodeModel.blockId)));
const viewType = useAtomValue(waveEnv.getBlockMetaKeyAtom(props.nodeModel.blockId, "view")) ?? "";
if (isNull || isBlank(props.nodeModel.blockId)) {
return null;
}
return <BlockInner key={props.nodeModel.blockId + ":" + viewType} {...props} viewType={viewType} />;
});

const SubBlockInner = memo((props: SubBlockProps & { viewType: string }) => {
counterInc("render-Block");
counterInc("render-Block-" + props.nodeModel?.blockId?.substring(0, 8));
counterInc("render-Block-" + props.nodeModel.blockId?.substring(0, 8));
const tabModel = useTabModel();
const waveEnv = useWaveEnv();
const [blockData, loading] = useWaveObjectValue<Block>(makeORef("block", props.nodeModel.blockId));
const bcm = getBlockComponentModel(props.nodeModel.blockId);
let viewModel = bcm?.viewModel;
if (viewModel == null || viewModel.viewType != blockData?.meta?.view) {
viewModel = makeViewModel(props.nodeModel.blockId, blockData?.meta?.view, props.nodeModel, tabModel, waveEnv);
if (viewModel == null) {
// viewModel gets the full waveEnv
viewModel = makeViewModel(props.nodeModel.blockId, props.viewType, props.nodeModel, tabModel, waveEnv);
registerBlockComponentModel(props.nodeModel.blockId, { viewModel });
}
useEffect(() => {
Expand All @@ -356,10 +356,18 @@ const SubBlock = memo((props: SubBlockProps) => {
viewModel?.dispose?.();
};
}, []);
if (loading || isBlank(props.nodeModel.blockId) || blockData == null) {
return <BlockSubBlock {...props} viewModel={viewModel} />;
});
SubBlockInner.displayName = "SubBlockInner";

const SubBlock = memo((props: SubBlockProps) => {
const waveEnv = useWaveEnv<BlockEnv>();
const isNull = useAtomValue(waveEnv.wos.isWaveObjectNullAtom(makeORef("block", props.nodeModel.blockId)));
const viewType = useAtomValue(waveEnv.getBlockMetaKeyAtom(props.nodeModel.blockId, "view")) ?? "";
if (isNull || isBlank(props.nodeModel.blockId)) {
return null;
}
return <BlockSubBlock {...props} viewModel={viewModel} />;
return <SubBlockInner key={props.nodeModel.blockId + ":" + viewType} {...props} viewType={viewType} />;
});

export { Block, SubBlock };
42 changes: 42 additions & 0 deletions frontend/app/block/blockenv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import { BlockMetaKeyAtomFnType, ConnConfigKeyAtomFnType, SettingsKeyAtomFnType, WaveEnv } from "@/app/waveenv/waveenv";

export type BlockEnv = {
getSettingsKeyAtom: SettingsKeyAtomFnType<
| "app:focusfollowscursor"
| "app:showoverlayblocknums"
| "window:magnifiedblockblurprimarypx"
| "window:magnifiedblockopacity"
>;
atoms: {
modalOpen: WaveEnv["atoms"]["modalOpen"];
controlShiftDelayAtom: WaveEnv["atoms"]["controlShiftDelayAtom"];
};
electron: {
openExternal: WaveEnv["electron"]["openExternal"];
};
rpc: {
ActivityCommand: WaveEnv["rpc"]["ActivityCommand"];
ConnEnsureCommand: WaveEnv["rpc"]["ConnEnsureCommand"];
ConnDisconnectCommand: WaveEnv["rpc"]["ConnDisconnectCommand"];
ConnConnectCommand: WaveEnv["rpc"]["ConnConnectCommand"];
SetConnectionsConfigCommand: WaveEnv["rpc"]["SetConnectionsConfigCommand"];
DismissWshFailCommand: WaveEnv["rpc"]["DismissWshFailCommand"];
};
wos: WaveEnv["wos"];
getConnStatusAtom: WaveEnv["getConnStatusAtom"];
getLocalHostDisplayNameAtom: WaveEnv["getLocalHostDisplayNameAtom"];
getConnConfigKeyAtom: ConnConfigKeyAtomFnType<"conn:wshenabled">;
getBlockMetaKeyAtom: BlockMetaKeyAtomFnType<
| "frame:text"
| "frame:activebordercolor"
| "frame:bordercolor"
| "view"
| "connection"
| "icon:color"
| "frame:title"
| "frame:icon"
>;
};
41 changes: 25 additions & 16 deletions frontend/app/block/blockframe-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import { ConnectionButton } from "@/app/block/connectionbutton";
import { DurableSessionFlyover } from "@/app/block/durable-session-flyover";
import { getBlockBadgeAtom } from "@/app/store/badge";
import { ContextMenuModel } from "@/app/store/contextmenu";
import { recordTEvent, refocusNode, WOS } from "@/app/store/global";
import { recordTEvent, refocusNode } from "@/app/store/global";
import { globalStore } from "@/app/store/jotaiStore";
import { uxCloseBlock } from "@/app/store/keymodel";
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { useWaveEnv } from "@/app/waveenv/waveenv";
import { BlockEnv } from "./blockenv";
import { IconButton } from "@/element/iconbutton";
import { NodeModel } from "@/layout/index";
import * as util from "@/util/util";
Expand All @@ -34,7 +35,7 @@ function handleHeaderContextMenu(
e.preventDefault();
e.stopPropagation();
const magnified = globalStore.get(nodeModel.isMagnified);
let menu: ContextMenuItem[] = [
const menu: ContextMenuItem[] = [
{
label: magnified ? "Un-Magnify Block" : "Magnify Block",
click: () => {
Expand Down Expand Up @@ -63,14 +64,17 @@ function handleHeaderContextMenu(

type HeaderTextElemsProps = {
viewModel: ViewModel;
blockData: Block;
blockId: string;
preview: boolean;
error?: Error;
};

const HeaderTextElems = React.memo(({ viewModel, blockData, preview, error }: HeaderTextElemsProps) => {
const HeaderTextElems = React.memo(({ viewModel, blockId, preview, error }: HeaderTextElemsProps) => {
const waveEnv = useWaveEnv<BlockEnv>();
const frameTextAtom = waveEnv.getBlockMetaKeyAtom(blockId, "frame:text");
const frameText = jotai.useAtomValue(frameTextAtom);
let headerTextUnion = util.useAtomValueSafe(viewModel?.viewText);
headerTextUnion = blockData?.meta?.["frame:text"] ?? headerTextUnion;
headerTextUnion = frameText ?? headerTextUnion;

const headerTextElems: React.ReactElement[] = [];
if (typeof headerTextUnion === "string") {
Expand Down Expand Up @@ -171,9 +175,13 @@ const BlockFrame_Header = ({
changeConnModalAtom,
error,
}: BlockFrameProps & { changeConnModalAtom: jotai.PrimitiveAtom<boolean>; error?: Error }) => {
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
let viewName = util.useAtomValueSafe(viewModel?.viewName) ?? blockViewToName(blockData?.meta?.view);
let viewIconUnion = util.useAtomValueSafe(viewModel?.viewIcon) ?? blockViewToIcon(blockData?.meta?.view);
const waveEnv = useWaveEnv<BlockEnv>();
const metaView = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "view"));
const metaFrameTitle = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "frame:title"));
const metaFrameIcon = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "frame:icon"));
const metaConnection = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "connection"));
let viewName = util.useAtomValueSafe(viewModel?.viewName) ?? blockViewToName(metaView);
let viewIconUnion = util.useAtomValueSafe(viewModel?.viewIcon) ?? blockViewToIcon(metaView);
const preIconButton = util.useAtomValueSafe(viewModel?.preIconButton);
const useTermHeader = util.useAtomValueSafe(viewModel?.useTermHeader);
const termConfigedDurable = util.useAtomValueSafe(viewModel?.termConfigedDurable);
Expand All @@ -182,20 +190,21 @@ const BlockFrame_Header = ({
const magnified = jotai.useAtomValue(nodeModel.isMagnified);
const prevMagifiedState = React.useRef(magnified);
const manageConnection = util.useAtomValueSafe(viewModel?.manageConnection);
const iconColor = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "icon:color"));
const dragHandleRef = preview ? null : nodeModel.dragHandleRef;
const isTerminalBlock = blockData?.meta?.view === "term";
viewName = blockData?.meta?.["frame:title"] ?? viewName;
viewIconUnion = blockData?.meta?.["frame:icon"] ?? viewIconUnion;
const isTerminalBlock = metaView === "term";
viewName = metaFrameTitle ?? viewName;
viewIconUnion = metaFrameIcon ?? viewIconUnion;

React.useEffect(() => {
if (magnified && !preview && !prevMagifiedState.current) {
RpcApi.ActivityCommand(TabRpcClient, { nummagnify: 1 });
waveEnv.rpc.ActivityCommand(TabRpcClient, { nummagnify: 1 });
recordTEvent("action:magnify", { "block:view": viewName });
}
prevMagifiedState.current = magnified;
}, [magnified]);

const viewIconElem = getViewIconElem(viewIconUnion, blockData);
const viewIconElem = getViewIconElem(viewIconUnion, iconColor);

return (
<div
Expand All @@ -217,7 +226,7 @@ const BlockFrame_Header = ({
<ConnectionButton
ref={connBtnRef}
key="connbutton"
connection={blockData?.meta?.connection}
connection={metaConnection}
changeConnModalAtom={changeConnModalAtom}
isTerminalBlock={isTerminalBlock}
/>
Expand All @@ -236,7 +245,7 @@ const BlockFrame_Header = ({
<i className={makeIconClass(badge.icon, true, { defaultIcon: "circle-small" })} />
</div>
)}
<HeaderTextElems viewModel={viewModel} blockData={blockData} preview={preview} error={error} />
<HeaderTextElems viewModel={viewModel} blockId={nodeModel.blockId} preview={preview} error={error} />
<HeaderEndIcons viewModel={viewModel} nodeModel={nodeModel} blockId={nodeModel.blockId} />
</div>
);
Expand Down
Loading
Loading