mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
dynamic header updates (#102)
This commit is contained in:
parent
0cb9b8940b
commit
848a9af9a0
@ -69,8 +69,8 @@
|
|||||||
|
|
||||||
.block-frame-default-header {
|
.block-frame-default-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 26px;
|
height: 34px;
|
||||||
padding-left: 6px;
|
padding: 4px 0 4px 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
@ -84,6 +84,10 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
color: var(--main-text-color);
|
color: var(--main-text-color);
|
||||||
|
|
||||||
|
.block-frame-back-button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.block-frame-view-icon {
|
.block-frame-view-icon {
|
||||||
font-size: var(--header-icon-size);
|
font-size: var(--header-icon-size);
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
@ -103,6 +107,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block-frame-text {
|
||||||
|
font: var(--fixed-font);
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
.block-frame-end-icons {
|
.block-frame-end-icons {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -139,109 +149,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.block-frame-tech {
|
|
||||||
border: 2px solid var(--border-color);
|
|
||||||
border-radius: 7px;
|
|
||||||
margin: 8px 0 0 0;
|
|
||||||
padding: 10px 2px 2px 2px;
|
|
||||||
height: calc(100% - 8px);
|
|
||||||
width: 100%;
|
|
||||||
overflow: visible;
|
|
||||||
|
|
||||||
.block-header-icon {
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.block-preview {
|
|
||||||
background-color: var(--main-bg-color);
|
|
||||||
|
|
||||||
.block-frame-tech-close {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.block-focused {
|
|
||||||
border: 2px solid var(--accent-color);
|
|
||||||
|
|
||||||
.block-frame-tech-header {
|
|
||||||
color: var(--main-text-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-frame-tech-header {
|
|
||||||
position: absolute;
|
|
||||||
max-width: 85%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, 0);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
top: -11px;
|
|
||||||
padding: 4px 6px 4px 6px;
|
|
||||||
background-color: var(--main-bg-color);
|
|
||||||
font: var(--fixed-font);
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
white-space: nowrap;
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-frame-tech-close {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: -2px;
|
|
||||||
padding: 0 0 1px 1px;
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-frame-preview {
|
.block-frame-preview {
|
||||||
background-color: var(--main-bg-color);
|
background-color: var(--main-bg-color);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-shrink: 0;
|
|
||||||
height: 30px;
|
|
||||||
width: 100%;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: var(--panel-bg-color);
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
cursor: default;
|
|
||||||
border-radius: 0 0 4px 4px;
|
|
||||||
cursor: grab;
|
|
||||||
|
|
||||||
.block-header-icon {
|
|
||||||
font-size: 20px;
|
|
||||||
padding: 0 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-header-text {
|
|
||||||
padding: 0 5px;
|
|
||||||
flex-grow: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-button {
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 5px 5px 5px 5px;
|
|
||||||
margin-right: 5px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--highlight-bg-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,12 +1,6 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import { CodeEdit } from "@/app/view/codeedit";
|
|
||||||
import { PlotView } from "@/app/view/plotview";
|
|
||||||
import { PreviewView } from "@/app/view/preview";
|
|
||||||
import { TerminalView } from "@/app/view/term/term";
|
|
||||||
import { WaveAi } from "@/app/view/waveai";
|
|
||||||
import { WebView } from "@/app/view/webview";
|
|
||||||
import { ErrorBoundary } from "@/element/errorboundary";
|
import { ErrorBoundary } from "@/element/errorboundary";
|
||||||
import { CenteredDiv } from "@/element/quickelems";
|
import { CenteredDiv } from "@/element/quickelems";
|
||||||
import { ContextMenuModel } from "@/store/contextmenu";
|
import { ContextMenuModel } from "@/store/contextmenu";
|
||||||
@ -14,19 +8,43 @@ import { atoms, globalStore, setBlockFocus, useBlockAtom } from "@/store/global"
|
|||||||
import * as services from "@/store/services";
|
import * as services from "@/store/services";
|
||||||
import * as WOS from "@/store/wos";
|
import * as WOS from "@/store/wos";
|
||||||
import * as util from "@/util/util";
|
import * as util from "@/util/util";
|
||||||
|
import { CodeEdit } from "@/view/codeedit";
|
||||||
|
import { PlotView } from "@/view/plotview";
|
||||||
|
import { PreviewView, makePreviewModel } from "@/view/preview";
|
||||||
|
import { TerminalView } from "@/view/term/term";
|
||||||
|
import { WaveAi } from "@/view/waveai";
|
||||||
|
import { WebView } from "@/view/webview";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import * as jotai from "jotai";
|
import * as jotai from "jotai";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import "./block.less";
|
import "./block.less";
|
||||||
|
|
||||||
const HoverPixels = 15;
|
interface LayoutComponentModel {
|
||||||
const HoverTimeoutMs = 100;
|
onClose?: () => void;
|
||||||
|
dragHandleRef?: React.RefObject<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
interface BlockProps {
|
interface BlockProps {
|
||||||
blockId: string;
|
blockId: string;
|
||||||
onClose?: () => void;
|
preview: boolean;
|
||||||
dragHandleRef?: React.RefObject<HTMLDivElement>;
|
layoutModel: LayoutComponentModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlockComponentModel {
|
||||||
|
onClick?: () => void;
|
||||||
|
onFocusCapture?: React.FocusEventHandler<HTMLDivElement>;
|
||||||
|
blockRef?: React.RefObject<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlockFrameProps {
|
||||||
|
blockId: string;
|
||||||
|
blockModel?: BlockComponentModel;
|
||||||
|
layoutModel?: LayoutComponentModel;
|
||||||
|
viewModel?: ViewModel;
|
||||||
|
preview: boolean;
|
||||||
|
numBlocksInTab?: number;
|
||||||
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const colorRegex = /^((#[0-9a-f]{6,8})|([a-z]+))$/;
|
const colorRegex = /^((#[0-9a-f]{6,8})|([a-z]+))$/;
|
||||||
@ -176,118 +194,13 @@ function handleHeaderContextMenu(e: React.MouseEvent<HTMLDivElement>, blockData:
|
|||||||
ContextMenuModel.showContextMenu(menu, e);
|
ContextMenuModel.showContextMenu(menu, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FramelessBlockHeaderProps {
|
|
||||||
blockId: string;
|
|
||||||
onClose?: () => void;
|
|
||||||
dragHandleRef?: React.RefObject<HTMLDivElement>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FramelessBlockHeader = React.memo(({ blockId, onClose, dragHandleRef }: FramelessBlockHeaderProps) => {
|
|
||||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
|
||||||
const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key="header"
|
|
||||||
className="block-header"
|
|
||||||
ref={dragHandleRef}
|
|
||||||
onContextMenu={(e) => handleHeaderContextMenu(e, blockData, onClose)}
|
|
||||||
>
|
|
||||||
<div className="block-header-text text-fixed">{getBlockHeaderText(null, blockData, settingsConfig)}</div>
|
|
||||||
{onClose && (
|
|
||||||
<div className="close-button" onClick={onClose}>
|
|
||||||
<i className="fa fa-solid fa-xmark-large" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const hoverStateOff = "off";
|
|
||||||
const hoverStatePending = "pending";
|
|
||||||
const hoverStateOn = "on";
|
|
||||||
|
|
||||||
interface BlockFrameProps {
|
|
||||||
blockId: string;
|
|
||||||
onClose?: () => void;
|
|
||||||
onClick?: () => void;
|
|
||||||
onFocusCapture?: React.FocusEventHandler<HTMLDivElement>;
|
|
||||||
preview: boolean;
|
|
||||||
numBlocksInTab?: number;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
blockRef?: React.RefObject<HTMLDivElement>;
|
|
||||||
dragHandleRef?: React.RefObject<HTMLDivElement>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BlockFrame_Tech_Component = ({
|
|
||||||
blockId,
|
|
||||||
onClose,
|
|
||||||
onClick,
|
|
||||||
onFocusCapture,
|
|
||||||
preview,
|
|
||||||
blockRef,
|
|
||||||
dragHandleRef,
|
|
||||||
children,
|
|
||||||
}: BlockFrameProps) => {
|
|
||||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
|
||||||
const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);
|
|
||||||
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
|
|
||||||
return jotai.atom((get) => {
|
|
||||||
const winData = get(atoms.waveWindow);
|
|
||||||
return winData?.activeblockid === blockId;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
let isFocused = jotai.useAtomValue(isFocusedAtom);
|
|
||||||
const blockIcon = useBlockIcon(blockId);
|
|
||||||
if (preview) {
|
|
||||||
isFocused = true;
|
|
||||||
}
|
|
||||||
let style: React.CSSProperties = {};
|
|
||||||
if (!isFocused && blockData?.meta?.["frame:bordercolor"]) {
|
|
||||||
style.borderColor = blockData.meta["frame:bordercolor"];
|
|
||||||
}
|
|
||||||
if (isFocused && blockData?.meta?.["frame:bordercolor:focused"]) {
|
|
||||||
style.borderColor = blockData.meta["frame:bordercolor:focused"];
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
"block",
|
|
||||||
"block-frame-tech",
|
|
||||||
isFocused ? "block-focused" : null,
|
|
||||||
preview ? "block-preview" : null
|
|
||||||
)}
|
|
||||||
onClick={onClick}
|
|
||||||
onFocusCapture={onFocusCapture}
|
|
||||||
ref={blockRef}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="block-frame-tech-header"
|
|
||||||
ref={dragHandleRef}
|
|
||||||
onContextMenu={(e) => handleHeaderContextMenu(e, blockData, onClose)}
|
|
||||||
>
|
|
||||||
{getBlockHeaderText(blockIcon, blockData, settingsConfig)}
|
|
||||||
</div>
|
|
||||||
<div className={clsx("block-frame-tech-close")} onClick={onClose}>
|
|
||||||
<i className="fa fa-solid fa-xmark fa-fw" />
|
|
||||||
</div>
|
|
||||||
{preview ? <div className="block-frame-preview" /> : children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const BlockFrame_Tech = React.memo(BlockFrame_Tech_Component) as typeof BlockFrame_Tech_Component;
|
|
||||||
|
|
||||||
const BlockFrame_Default_Component = ({
|
const BlockFrame_Default_Component = ({
|
||||||
blockId,
|
blockId,
|
||||||
onClose,
|
layoutModel,
|
||||||
onClick,
|
viewModel,
|
||||||
|
blockModel,
|
||||||
preview,
|
preview,
|
||||||
blockRef,
|
|
||||||
dragHandleRef,
|
|
||||||
numBlocksInTab,
|
numBlocksInTab,
|
||||||
onFocusCapture,
|
|
||||||
children,
|
children,
|
||||||
}: BlockFrameProps) => {
|
}: BlockFrameProps) => {
|
||||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||||
@ -299,7 +212,10 @@ const BlockFrame_Default_Component = ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
let isFocused = jotai.useAtomValue(isFocusedAtom);
|
let isFocused = jotai.useAtomValue(isFocusedAtom);
|
||||||
const blockIcon = useBlockIcon(blockId);
|
const viewIcon = jotai.useAtomValue(viewModel.viewIcon);
|
||||||
|
const viewText = jotai.useAtomValue(viewModel.viewText);
|
||||||
|
const hasBackButton = jotai.useAtomValue(viewModel.hasBackButton);
|
||||||
|
const hasForwardButton = jotai.useAtomValue(viewModel.hasForwardButton);
|
||||||
if (preview) {
|
if (preview) {
|
||||||
isFocused = true;
|
isFocused = true;
|
||||||
}
|
}
|
||||||
@ -339,7 +255,7 @@ const BlockFrame_Default_Component = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
menuItems.push({ type: "separator" });
|
menuItems.push({ type: "separator" });
|
||||||
menuItems.push({ label: "Close", click: onClose });
|
menuItems.push({ label: "Close", click: layoutModel?.onClose });
|
||||||
ContextMenuModel.showContextMenu(menuItems, e);
|
ContextMenuModel.showContextMenu(menuItems, e);
|
||||||
};
|
};
|
||||||
if (preview) {
|
if (preview) {
|
||||||
@ -355,29 +271,35 @@ const BlockFrame_Default_Component = ({
|
|||||||
preview ? "block-preview" : null,
|
preview ? "block-preview" : null,
|
||||||
numBlocksInTab == 1 ? "block-no-highlight" : null
|
numBlocksInTab == 1 ? "block-no-highlight" : null
|
||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={blockModel?.onClick}
|
||||||
onFocusCapture={onFocusCapture}
|
onFocusCapture={blockModel?.onFocusCapture}
|
||||||
ref={blockRef}
|
ref={blockModel?.blockRef}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="block-frame-default-header"
|
className="block-frame-default-header"
|
||||||
ref={dragHandleRef}
|
ref={layoutModel?.dragHandleRef}
|
||||||
onContextMenu={(e) => handleHeaderContextMenu(e, blockData, onClose)}
|
onContextMenu={(e) => handleHeaderContextMenu(e, blockData, layoutModel?.onClose)}
|
||||||
>
|
>
|
||||||
<div className="block-frame-default-header-iconview">
|
<div className="block-frame-default-header-iconview">
|
||||||
<div className="block-frame-view-icon">{getBlockHeaderIcon(blockIcon, blockData)}</div>
|
{hasBackButton && !hasForwardButton && (
|
||||||
|
<div className="block-frame-back-button" onClick={viewModel.onBack}>
|
||||||
|
<i className="fa fa-solid fa-chevron-left fa-fw" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="block-frame-view-icon">{getBlockHeaderIcon(viewIcon, blockData)}</div>
|
||||||
<div className="block-frame-view-type">{blockViewToName(blockData?.view)}</div>
|
<div className="block-frame-view-type">{blockViewToName(blockData?.view)}</div>
|
||||||
{settingsConfig?.blockheader?.showblockids && (
|
{settingsConfig?.blockheader?.showblockids && (
|
||||||
<div className="block-frame-blockid">[{blockId.substring(0, 8)}]</div>
|
<div className="block-frame-blockid">[{blockId.substring(0, 8)}]</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{util.isBlank(viewText) ? null : <div className="block-frame-text">{viewText}</div>}
|
||||||
<div className="flex-spacer"></div>
|
<div className="flex-spacer"></div>
|
||||||
<div className="block-frame-end-icons">
|
<div className="block-frame-end-icons">
|
||||||
<div className="block-frame-settings" onClick={handleSettings}>
|
<div className="block-frame-settings" onClick={handleSettings}>
|
||||||
<i className="fa fa-solid fa-cog fa-fw" />
|
<i className="fa fa-solid fa-cog fa-fw" />
|
||||||
</div>
|
</div>
|
||||||
<div className={clsx("block-frame-default-close")} onClick={onClose}>
|
<div className={clsx("block-frame-default-close")} onClick={layoutModel?.onClose}>
|
||||||
<i className="fa fa-solid fa-xmark-large fa-fw" />
|
<i className="fa fa-solid fa-xmark-large fa-fw" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -390,80 +312,6 @@ const BlockFrame_Default_Component = ({
|
|||||||
|
|
||||||
const BlockFrame_Default = React.memo(BlockFrame_Default_Component) as typeof BlockFrame_Default_Component;
|
const BlockFrame_Default = React.memo(BlockFrame_Default_Component) as typeof BlockFrame_Default_Component;
|
||||||
|
|
||||||
const BlockFrame_Frameless = ({
|
|
||||||
blockId,
|
|
||||||
onClose,
|
|
||||||
onClick,
|
|
||||||
preview,
|
|
||||||
blockRef,
|
|
||||||
dragHandleRef,
|
|
||||||
children,
|
|
||||||
}: BlockFrameProps) => {
|
|
||||||
const localBlockRef = React.useRef<HTMLDivElement>(null);
|
|
||||||
const [showHeader, setShowHeader] = React.useState(preview ? true : false);
|
|
||||||
const hoverState = React.useRef(hoverStateOff);
|
|
||||||
|
|
||||||
// this forward the lcal ref to the blockRef
|
|
||||||
React.useImperativeHandle(blockRef, () => localBlockRef.current);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (preview) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const block = localBlockRef.current;
|
|
||||||
let hoverTimeout: NodeJS.Timeout = null;
|
|
||||||
const handleMouseMove = (event) => {
|
|
||||||
const rect = block.getBoundingClientRect();
|
|
||||||
if (event.clientY - rect.top <= HoverPixels) {
|
|
||||||
if (hoverState.current == hoverStateOff) {
|
|
||||||
hoverTimeout = setTimeout(() => {
|
|
||||||
if (hoverState.current == hoverStatePending) {
|
|
||||||
hoverState.current = hoverStateOn;
|
|
||||||
setShowHeader(true);
|
|
||||||
}
|
|
||||||
}, HoverTimeoutMs);
|
|
||||||
hoverState.current = hoverStatePending;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (hoverTimeout) {
|
|
||||||
if (hoverState.current == hoverStatePending) {
|
|
||||||
hoverState.current = hoverStateOff;
|
|
||||||
}
|
|
||||||
clearTimeout(hoverTimeout);
|
|
||||||
hoverTimeout = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
block.addEventListener("mousemove", handleMouseMove);
|
|
||||||
return () => {
|
|
||||||
block.removeEventListener("mousemove", handleMouseMove);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
let mouseLeaveHandler = () => {
|
|
||||||
if (preview) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setShowHeader(false);
|
|
||||||
hoverState.current = hoverStateOff;
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="block block-frame-frameless"
|
|
||||||
ref={localBlockRef}
|
|
||||||
onMouseLeave={mouseLeaveHandler}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={clsx("block-header-animation-wrap", showHeader ? "is-showing" : null)}
|
|
||||||
onMouseLeave={mouseLeaveHandler}
|
|
||||||
>
|
|
||||||
<FramelessBlockHeader blockId={blockId} onClose={onClose} dragHandleRef={dragHandleRef} />
|
|
||||||
</div>
|
|
||||||
{preview ? <div className="block-frame-preview" /> : children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const BlockFrame = React.memo((props: BlockFrameProps) => {
|
const BlockFrame = React.memo((props: BlockFrameProps) => {
|
||||||
const blockId = props.blockId;
|
const blockId = props.blockId;
|
||||||
const [blockData, blockDataLoading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
const [blockData, blockDataLoading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||||
@ -474,18 +322,6 @@ const BlockFrame = React.memo((props: BlockFrameProps) => {
|
|||||||
}
|
}
|
||||||
let FrameElem = BlockFrame_Default;
|
let FrameElem = BlockFrame_Default;
|
||||||
const numBlocks = tabData?.blockids?.length ?? 0;
|
const numBlocks = tabData?.blockids?.length ?? 0;
|
||||||
// Preview should always render as the full tech frame
|
|
||||||
if (!props.preview) {
|
|
||||||
// if 0 or 1 blocks, use frameless, otherwise use tech
|
|
||||||
// if (numBlocks <= 1) {
|
|
||||||
// FrameElem = BlockFrame_Frameless;
|
|
||||||
// }
|
|
||||||
if (blockData?.meta?.["frame"] === "tech") {
|
|
||||||
FrameElem = BlockFrame_Tech;
|
|
||||||
} else if (blockData?.meta?.["frame"] === "frameless") {
|
|
||||||
FrameElem = BlockFrame_Frameless;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return <FrameElem {...props} numBlocksInTab={numBlocks} />;
|
return <FrameElem {...props} numBlocksInTab={numBlocks} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -547,17 +383,78 @@ function useBlockIcon(blockId: string): string {
|
|||||||
return blockIcon;
|
return blockIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wm = new WeakMap();
|
function getViewElemAndModel(
|
||||||
let wmCounter = 0;
|
blockId: string,
|
||||||
function getObjectId(obj: any): number {
|
blockView: string,
|
||||||
if (!wm.has(obj)) {
|
blockRef: React.RefObject<HTMLDivElement>
|
||||||
wm.set(obj, wmCounter++);
|
): { viewModel: ViewModel; viewElem: JSX.Element } {
|
||||||
|
if (blockView == null) {
|
||||||
|
return { viewElem: null, viewModel: null };
|
||||||
}
|
}
|
||||||
return wm.get(obj);
|
let viewElem: JSX.Element = null;
|
||||||
|
let viewModel: ViewModel = null;
|
||||||
|
if (blockView === "term") {
|
||||||
|
viewElem = <TerminalView key={blockId} blockId={blockId} />;
|
||||||
|
} else if (blockView === "preview") {
|
||||||
|
const previewModel = makePreviewModel(blockId);
|
||||||
|
viewElem = <PreviewView key={blockId} blockId={blockId} model={previewModel} />;
|
||||||
|
viewModel = previewModel;
|
||||||
|
} else if (blockView === "plot") {
|
||||||
|
viewElem = <PlotView key={blockId} />;
|
||||||
|
} else if (blockView === "codeedit") {
|
||||||
|
viewElem = <CodeEdit key={blockId} text={null} filename={null} />;
|
||||||
|
} else if (blockView === "web") {
|
||||||
|
viewElem = <WebView key={blockId} blockId={blockId} parentRef={blockRef} />;
|
||||||
|
} else if (blockView === "waveai") {
|
||||||
|
viewElem = <WaveAi key={blockId} parentRef={blockRef} />;
|
||||||
|
}
|
||||||
|
if (viewModel == null) {
|
||||||
|
viewModel = makeDefaultViewModel(blockId);
|
||||||
|
}
|
||||||
|
return { viewElem, viewModel };
|
||||||
}
|
}
|
||||||
|
|
||||||
const Block = React.memo(({ blockId, onClose, dragHandleRef }: BlockProps) => {
|
function makeDefaultViewModel(blockId: string): ViewModel {
|
||||||
let blockElem: JSX.Element = null;
|
const blockDataAtom = WOS.getWaveObjectAtom<Block>(WOS.makeORef("block", blockId));
|
||||||
|
let viewModel: ViewModel = {
|
||||||
|
viewIcon: jotai.atom((get) => {
|
||||||
|
const blockData = get(blockDataAtom);
|
||||||
|
return blockViewToIcon(blockData?.view);
|
||||||
|
}),
|
||||||
|
viewName: jotai.atom((get) => {
|
||||||
|
const blockData = get(blockDataAtom);
|
||||||
|
return blockViewToName(blockData?.view);
|
||||||
|
}),
|
||||||
|
viewText: jotai.atom((get) => {
|
||||||
|
const blockData = get(blockDataAtom);
|
||||||
|
return blockData?.meta?.title;
|
||||||
|
}),
|
||||||
|
hasBackButton: jotai.atom(false),
|
||||||
|
hasForwardButton: jotai.atom(false),
|
||||||
|
hasSearch: jotai.atom(false),
|
||||||
|
};
|
||||||
|
return viewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlockPreview = React.memo(({ blockId, layoutModel }: BlockProps) => {
|
||||||
|
const [blockData, blockDataLoading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||||
|
if (!blockData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let { viewModel } = getViewElemAndModel(blockId, blockData?.view, null);
|
||||||
|
return (
|
||||||
|
<BlockFrame
|
||||||
|
key={blockId}
|
||||||
|
blockId={blockId}
|
||||||
|
layoutModel={layoutModel}
|
||||||
|
preview={true}
|
||||||
|
blockModel={null}
|
||||||
|
viewModel={viewModel}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const BlockFull = React.memo(({ blockId, layoutModel }: BlockProps) => {
|
||||||
const focusElemRef = React.useRef<HTMLInputElement>(null);
|
const focusElemRef = React.useRef<HTMLInputElement>(null);
|
||||||
const blockRef = React.useRef<HTMLDivElement>(null);
|
const blockRef = React.useRef<HTMLDivElement>(null);
|
||||||
const [blockClicked, setBlockClicked] = React.useState(false);
|
const [blockClicked, setBlockClicked] = React.useState(false);
|
||||||
@ -626,33 +523,30 @@ const Block = React.memo(({ blockId, onClose, dragHandleRef }: BlockProps) => {
|
|||||||
}
|
}
|
||||||
}, [focusElemRef.current, getFocusableChildren]);
|
}, [focusElemRef.current, getFocusableChildren]);
|
||||||
|
|
||||||
|
let { viewElem, viewModel } = React.useMemo(
|
||||||
|
() => getViewElemAndModel(blockId, blockData?.view, blockRef),
|
||||||
|
[blockId, blockData?.view, blockRef]
|
||||||
|
);
|
||||||
|
|
||||||
if (!blockId || !blockData) return null;
|
if (!blockId || !blockData) return null;
|
||||||
|
|
||||||
if (blockDataLoading) {
|
if (blockDataLoading) {
|
||||||
blockElem = <CenteredDiv>Loading...</CenteredDiv>;
|
viewElem = <CenteredDiv>Loading...</CenteredDiv>;
|
||||||
} else if (blockData.view === "term") {
|
|
||||||
blockElem = <TerminalView key={blockId} blockId={blockId} />;
|
|
||||||
} else if (blockData.view === "preview") {
|
|
||||||
blockElem = <PreviewView key={blockId} blockId={blockId} />;
|
|
||||||
} else if (blockData.view === "plot") {
|
|
||||||
blockElem = <PlotView key={blockId} />;
|
|
||||||
} else if (blockData.view === "codeedit") {
|
|
||||||
blockElem = <CodeEdit key={blockId} text={null} filename={null} />;
|
|
||||||
} else if (blockData.view === "web") {
|
|
||||||
blockElem = <WebView key={blockId} parentRef={blockRef} initialUrl={blockData.meta.url} />;
|
|
||||||
} else if (blockData.view === "waveai") {
|
|
||||||
blockElem = <WaveAi key={blockId} parentRef={blockRef} />;
|
|
||||||
}
|
}
|
||||||
|
const blockModel: BlockComponentModel = {
|
||||||
|
onClick: setBlockClickedTrue,
|
||||||
|
onFocusCapture: determineFocusedChild,
|
||||||
|
blockRef: blockRef,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BlockFrame
|
<BlockFrame
|
||||||
key={blockId}
|
key={blockId}
|
||||||
blockId={blockId}
|
blockId={blockId}
|
||||||
onClose={onClose}
|
layoutModel={layoutModel}
|
||||||
preview={false}
|
preview={false}
|
||||||
onClick={setBlockClickedTrue}
|
blockModel={blockModel}
|
||||||
blockRef={blockRef}
|
viewModel={viewModel}
|
||||||
dragHandleRef={dragHandleRef}
|
|
||||||
onFocusCapture={(e) => determineFocusedChild(e)}
|
|
||||||
>
|
>
|
||||||
<div key="focuselem" className="block-focuselem">
|
<div key="focuselem" className="block-focuselem">
|
||||||
<input
|
<input
|
||||||
@ -666,11 +560,18 @@ const Block = React.memo(({ blockId, onClose, dragHandleRef }: BlockProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div key="content" className="block-content">
|
<div key="content" className="block-content">
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<React.Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{blockElem}</React.Suspense>
|
<React.Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</React.Suspense>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</BlockFrame>
|
</BlockFrame>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export { Block, BlockFrame };
|
const Block = React.memo((props: BlockProps) => {
|
||||||
|
if (props.preview) {
|
||||||
|
return <BlockPreview {...props} />;
|
||||||
|
}
|
||||||
|
return <BlockFull {...props} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
export { Block };
|
||||||
|
@ -353,6 +353,15 @@ function setBlockFocus(blockId: string) {
|
|||||||
WOS.setObjectValue(winData, globalStore.set, true);
|
WOS.setObjectValue(winData, globalStore.set, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const objectIdWeakMap = new WeakMap();
|
||||||
|
let objectIdCounter = 0;
|
||||||
|
function getObjectId(obj: any): number {
|
||||||
|
if (!objectIdWeakMap.has(obj)) {
|
||||||
|
objectIdWeakMap.set(obj, objectIdCounter++);
|
||||||
|
}
|
||||||
|
return objectIdWeakMap.get(obj);
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
WOS,
|
WOS,
|
||||||
atoms,
|
atoms,
|
||||||
@ -363,6 +372,7 @@ export {
|
|||||||
getEventORefSubject,
|
getEventORefSubject,
|
||||||
getEventSubject,
|
getEventSubject,
|
||||||
getFileSubject,
|
getFileSubject,
|
||||||
|
getObjectId,
|
||||||
globalStore,
|
globalStore,
|
||||||
globalWS,
|
globalWS,
|
||||||
initWS,
|
initWS,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright 2023, Command Line Inc.
|
// Copyright 2023, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import { Block, BlockFrame } from "@/app/block/block";
|
import { Block } from "@/app/block/block";
|
||||||
import { getApi } from "@/store/global";
|
import { getApi } from "@/store/global";
|
||||||
import * as services from "@/store/services";
|
import * as services from "@/store/services";
|
||||||
import * as WOS from "@/store/wos";
|
import * as WOS from "@/store/wos";
|
||||||
@ -32,18 +32,15 @@ const TabContent = React.memo(({ tabId }: { tabId: string }) => {
|
|||||||
if (!tabData.blockId || !ready) {
|
if (!tabData.blockId || !ready) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
const layoutModel = {
|
||||||
<Block
|
onClose: onClose,
|
||||||
key={tabData.blockId}
|
dragHandleRef: dragHandleRef,
|
||||||
blockId={tabData.blockId}
|
};
|
||||||
onClose={onClose}
|
return <Block key={tabData.blockId} blockId={tabData.blockId} layoutModel={layoutModel} preview={false} />;
|
||||||
dragHandleRef={dragHandleRef}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPreview(tabData: TabLayoutData) {
|
function renderPreview(tabData: TabLayoutData) {
|
||||||
return <BlockFrame key={tabData.blockId} blockId={tabData.blockId} preview={true} />;
|
return <Block key={tabData.blockId} blockId={tabData.blockId} layoutModel={null} preview={true} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onNodeDelete(data: TabLayoutData) {
|
function onNodeDelete(data: TabLayoutData) {
|
||||||
|
@ -466,6 +466,7 @@ interface DirectoryPreviewProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function DirectoryPreview({ fileNameAtom }: DirectoryPreviewProps) {
|
function DirectoryPreview({ fileNameAtom }: DirectoryPreviewProps) {
|
||||||
|
console.log("DirectoryPreview render");
|
||||||
const [searchText, setSearchText] = React.useState("");
|
const [searchText, setSearchText] = React.useState("");
|
||||||
const [focusIndex, setFocusIndex] = React.useState(0);
|
const [focusIndex, setFocusIndex] = React.useState(0);
|
||||||
const [content, setContent] = React.useState<FileInfo[]>([]);
|
const [content, setContent] = React.useState<FileInfo[]>([]);
|
||||||
|
@ -2,12 +2,13 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import { Markdown } from "@/element/markdown";
|
import { Markdown } from "@/element/markdown";
|
||||||
import { getBackendHostPort, globalStore, useBlockAtom, useBlockCache } from "@/store/global";
|
import { getBackendHostPort, getObjectId, globalStore, useBlockAtom } from "@/store/global";
|
||||||
import * as services from "@/store/services";
|
import * as services from "@/store/services";
|
||||||
import * as WOS from "@/store/wos";
|
import * as WOS from "@/store/wos";
|
||||||
import * as util from "@/util/util";
|
import * as util from "@/util/util";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import * as jotai from "jotai";
|
import * as jotai from "jotai";
|
||||||
|
import { loadable } from "jotai/utils";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { CenteredDiv } from "../element/quickelems";
|
import { CenteredDiv } from "../element/quickelems";
|
||||||
import { CodeEdit } from "./codeedit";
|
import { CodeEdit } from "./codeedit";
|
||||||
@ -17,6 +18,113 @@ import { DirectoryPreview } from "./directorypreview";
|
|||||||
import "./view.less";
|
import "./view.less";
|
||||||
|
|
||||||
const MaxFileSize = 1024 * 1024 * 10; // 10MB
|
const MaxFileSize = 1024 * 1024 * 10; // 10MB
|
||||||
|
const MaxCSVSize = 1024 * 1024 * 1; // 1MB
|
||||||
|
|
||||||
|
export class PreviewModel implements ViewModel {
|
||||||
|
blockId: string;
|
||||||
|
blockAtom: jotai.Atom<Block>;
|
||||||
|
viewIcon: jotai.Atom<string>;
|
||||||
|
viewName: jotai.Atom<string>;
|
||||||
|
viewText: jotai.Atom<string>;
|
||||||
|
hasBackButton: jotai.Atom<boolean>;
|
||||||
|
hasForwardButton: jotai.Atom<boolean>;
|
||||||
|
hasSearch: jotai.Atom<boolean>;
|
||||||
|
|
||||||
|
fileName: jotai.WritableAtom<string, [string], void>;
|
||||||
|
statFile: jotai.Atom<Promise<FileInfo>>;
|
||||||
|
fullFile: jotai.Atom<Promise<FullFile>>;
|
||||||
|
fileMimeType: jotai.Atom<Promise<string>>;
|
||||||
|
fileMimeTypeLoadable: jotai.Atom<Loadable<string>>;
|
||||||
|
fileContent: jotai.Atom<Promise<string>>;
|
||||||
|
|
||||||
|
setPreviewFileName(fileName: string) {
|
||||||
|
services.ObjectService.UpdateObjectMeta(`block:${this.blockId}`, { file: fileName });
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(blockId: string) {
|
||||||
|
this.blockId = blockId;
|
||||||
|
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
|
||||||
|
this.viewIcon = jotai.atom((get) => {
|
||||||
|
let blockData = get(this.blockAtom);
|
||||||
|
if (blockData?.meta?.icon) {
|
||||||
|
return blockData.meta.icon;
|
||||||
|
}
|
||||||
|
const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), "");
|
||||||
|
const fileName = get(this.fileName);
|
||||||
|
return iconForFile(mimeType, fileName);
|
||||||
|
});
|
||||||
|
this.viewName = jotai.atom("Preview");
|
||||||
|
this.viewText = jotai.atom((get) => {
|
||||||
|
return get(this.fileName);
|
||||||
|
});
|
||||||
|
this.hasBackButton = jotai.atom(true);
|
||||||
|
this.hasForwardButton = jotai.atom((get) => {
|
||||||
|
const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), "");
|
||||||
|
if (mimeType == "directory") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
this.hasSearch = jotai.atom(false);
|
||||||
|
|
||||||
|
this.fileName = jotai.atom<string, [string], void>(
|
||||||
|
(get) => {
|
||||||
|
return get(this.blockAtom)?.meta?.file;
|
||||||
|
},
|
||||||
|
(get, set, update) => {
|
||||||
|
services.ObjectService.UpdateObjectMeta(`block:${blockId}`, { file: update });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.statFile = jotai.atom<Promise<FileInfo>>(async (get) => {
|
||||||
|
const fileName = get(this.fileName);
|
||||||
|
if (fileName == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// const statFile = await FileService.StatFile(fileName);
|
||||||
|
console.log("PreviewModel calling StatFile", fileName);
|
||||||
|
const statFile = await services.FileService.StatFile(fileName);
|
||||||
|
return statFile;
|
||||||
|
});
|
||||||
|
this.fullFile = jotai.atom<Promise<FullFile>>(async (get) => {
|
||||||
|
const fileName = get(this.fileName);
|
||||||
|
if (fileName == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// const file = await FileService.ReadFile(fileName);
|
||||||
|
const file = await services.FileService.ReadFile(fileName);
|
||||||
|
return file;
|
||||||
|
});
|
||||||
|
this.fileMimeType = jotai.atom<Promise<string>>(async (get) => {
|
||||||
|
const fileInfo = await get(this.statFile);
|
||||||
|
return fileInfo?.mimetype;
|
||||||
|
});
|
||||||
|
this.fileMimeTypeLoadable = loadable(this.fileMimeType);
|
||||||
|
this.fileContent = jotai.atom<Promise<string>>(async (get) => {
|
||||||
|
const fullFile = await get(this.fullFile);
|
||||||
|
return util.base64ToString(fullFile?.data64);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.onBack = this.onBack.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onBack() {
|
||||||
|
const fileName = globalStore.get(this.fileName);
|
||||||
|
if (fileName == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const splitPath = fileName.split("/");
|
||||||
|
console.log("splitPath-1", splitPath);
|
||||||
|
splitPath.pop();
|
||||||
|
console.log("splitPath-2", splitPath);
|
||||||
|
const newPath = splitPath.join("/");
|
||||||
|
globalStore.set(this.fileName, newPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makePreviewModel(blockId: string): PreviewModel {
|
||||||
|
const previewModel = new PreviewModel(blockId);
|
||||||
|
return previewModel;
|
||||||
|
}
|
||||||
|
|
||||||
function DirNav({ cwdAtom }: { cwdAtom: jotai.WritableAtom<string, [string], void> }) {
|
function DirNav({ cwdAtom }: { cwdAtom: jotai.WritableAtom<string, [string], void> }) {
|
||||||
const [cwd, setCwd] = jotai.useAtom(cwdAtom);
|
const [cwd, setCwd] = jotai.useAtom(cwdAtom);
|
||||||
@ -139,6 +247,9 @@ function CSVViewPreview({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function iconForFile(mimeType: string, fileName: string): string {
|
function iconForFile(mimeType: string, fileName: string): string {
|
||||||
|
if (mimeType == null) {
|
||||||
|
mimeType = "unknown";
|
||||||
|
}
|
||||||
if (mimeType == "application/pdf") {
|
if (mimeType == "application/pdf") {
|
||||||
return "file-pdf";
|
return "file-pdf";
|
||||||
} else if (mimeType.startsWith("image/")) {
|
} else if (mimeType.startsWith("image/")) {
|
||||||
@ -153,6 +264,7 @@ function iconForFile(mimeType: string, fileName: string): string {
|
|||||||
return "file-csv";
|
return "file-csv";
|
||||||
} else if (
|
} else if (
|
||||||
mimeType.startsWith("text/") ||
|
mimeType.startsWith("text/") ||
|
||||||
|
mimeType == "application/sql" ||
|
||||||
(mimeType.startsWith("application/") &&
|
(mimeType.startsWith("application/") &&
|
||||||
(mimeType.includes("json") || mimeType.includes("yaml") || mimeType.includes("toml")))
|
(mimeType.includes("json") || mimeType.includes("yaml") || mimeType.includes("toml")))
|
||||||
) {
|
) {
|
||||||
@ -161,60 +273,21 @@ function iconForFile(mimeType: string, fileName: string): string {
|
|||||||
if (fileName == "~" || fileName == "~/") {
|
if (fileName == "~" || fileName == "~/") {
|
||||||
return "home";
|
return "home";
|
||||||
}
|
}
|
||||||
return "folder";
|
return "folder-open";
|
||||||
} else {
|
} else {
|
||||||
return "file";
|
return "file";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function PreviewView({ blockId }: { blockId: string }) {
|
function PreviewView({ blockId, model }: { blockId: string; model: PreviewModel }) {
|
||||||
|
console.log("render previewview", getObjectId(model));
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
|
const blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
|
||||||
const fileNameAtom: jotai.WritableAtom<string, [string], void> = useBlockCache(blockId, "preview:filename", () =>
|
const fileNameAtom = model.fileName;
|
||||||
jotai.atom<string, [string], void>(
|
const statFileAtom = model.statFile;
|
||||||
(get) => {
|
const fullFileAtom = model.fullFile;
|
||||||
return get(blockAtom)?.meta?.file;
|
const fileMimeTypeAtom = model.fileMimeType;
|
||||||
},
|
const fileContentAtom = model.fileContent;
|
||||||
(get, set, update) => {
|
|
||||||
const blockId = get(blockAtom)?.oid;
|
|
||||||
services.ObjectService.UpdateObjectMeta(`block:${blockId}`, { file: update });
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const statFileAtom = useBlockAtom(blockId, "preview:statfile", () =>
|
|
||||||
jotai.atom<Promise<FileInfo>>(async (get) => {
|
|
||||||
const fileName = get(fileNameAtom);
|
|
||||||
if (fileName == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// const statFile = await FileService.StatFile(fileName);
|
|
||||||
const statFile = await services.FileService.StatFile(fileName);
|
|
||||||
return statFile;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const fullFileAtom = useBlockAtom(blockId, "preview:fullfile", () =>
|
|
||||||
jotai.atom<Promise<FullFile>>(async (get) => {
|
|
||||||
const fileName = get(fileNameAtom);
|
|
||||||
if (fileName == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// const file = await FileService.ReadFile(fileName);
|
|
||||||
const file = await services.FileService.ReadFile(fileName);
|
|
||||||
return file;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const fileMimeTypeAtom = useBlockAtom(blockId, "preview:mimetype", () =>
|
|
||||||
jotai.atom<Promise<string>>(async (get) => {
|
|
||||||
const fileInfo = await get(statFileAtom);
|
|
||||||
return fileInfo?.mimetype;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const fileContentAtom = useBlockAtom(blockId, "preview:filecontent", () =>
|
|
||||||
jotai.atom<Promise<string>>(async (get) => {
|
|
||||||
const fullFile = await get(fullFileAtom);
|
|
||||||
return util.base64ToString(fullFile?.data64);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
let mimeType = jotai.useAtomValue(fileMimeTypeAtom);
|
let mimeType = jotai.useAtomValue(fileMimeTypeAtom);
|
||||||
if (mimeType == null) {
|
if (mimeType == null) {
|
||||||
mimeType = "";
|
mimeType = "";
|
||||||
@ -241,11 +314,16 @@ function PreviewView({ blockId }: { blockId: string }) {
|
|||||||
} else if (mimeType === "text/markdown") {
|
} else if (mimeType === "text/markdown") {
|
||||||
specializedView = <MarkdownPreview contentAtom={fileContentAtom} />;
|
specializedView = <MarkdownPreview contentAtom={fileContentAtom} />;
|
||||||
} else if (mimeType === "text/csv") {
|
} else if (mimeType === "text/csv") {
|
||||||
|
if (fileInfo.size > MaxCSVSize) {
|
||||||
|
specializedView = <CenteredDiv>CSV File Too Large to Preview (1MB Max)</CenteredDiv>;
|
||||||
|
} else {
|
||||||
specializedView = (
|
specializedView = (
|
||||||
<CSVViewPreview parentRef={ref} contentAtom={fileContentAtom} filename={fileName} readonly={true} />
|
<CSVViewPreview parentRef={ref} contentAtom={fileContentAtom} filename={fileName} readonly={true} />
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} else if (
|
} else if (
|
||||||
mimeType.startsWith("text/") ||
|
mimeType.startsWith("text/") ||
|
||||||
|
mimeType == "application/sql" ||
|
||||||
(mimeType.startsWith("application/") &&
|
(mimeType.startsWith("application/") &&
|
||||||
(mimeType.includes("json") || mimeType.includes("yaml") || mimeType.includes("toml")))
|
(mimeType.includes("json") || mimeType.includes("yaml") || mimeType.includes("toml")))
|
||||||
) {
|
) {
|
||||||
@ -274,4 +352,4 @@ function PreviewView({ blockId }: { blockId: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { PreviewView };
|
export { PreviewView, makePreviewModel };
|
||||||
|
@ -4,17 +4,20 @@
|
|||||||
import { Button } from "@/app/element/button";
|
import { Button } from "@/app/element/button";
|
||||||
import { useParentHeight } from "@/app/hook/useParentHeight";
|
import { useParentHeight } from "@/app/hook/useParentHeight";
|
||||||
import { getApi } from "@/app/store/global";
|
import { getApi } from "@/app/store/global";
|
||||||
|
import { WOS } from "@/store/global";
|
||||||
import { WebviewTag } from "electron";
|
import { WebviewTag } from "electron";
|
||||||
import React, { memo, useEffect, useRef, useState } from "react";
|
import React, { memo, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import "./webview.less";
|
import "./webview.less";
|
||||||
|
|
||||||
interface WebViewProps {
|
interface WebViewProps {
|
||||||
|
blockId: string;
|
||||||
parentRef: React.MutableRefObject<HTMLDivElement>;
|
parentRef: React.MutableRefObject<HTMLDivElement>;
|
||||||
initialUrl: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const WebView = memo(({ parentRef, initialUrl }: WebViewProps) => {
|
const WebView = memo(({ blockId, parentRef }: WebViewProps) => {
|
||||||
|
const blockData = WOS.useWaveObjectValueWithSuspense<Block>(WOS.makeORef("block", blockId));
|
||||||
|
const initialUrl = useMemo(() => blockData?.meta?.url, []);
|
||||||
const [url, setUrl] = useState(initialUrl);
|
const [url, setUrl] = useState(initialUrl);
|
||||||
const [inputUrl, setInputUrl] = useState(initialUrl); // Separate state for the input field
|
const [inputUrl, setInputUrl] = useState(initialUrl); // Separate state for the input field
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
19
frontend/types/custom.d.ts
vendored
19
frontend/types/custom.d.ts
vendored
@ -1,6 +1,7 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import type * as jotai from "jotai";
|
||||||
import type * as rxjs from "rxjs";
|
import type * as rxjs from "rxjs";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@ -114,6 +115,24 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
|
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
|
||||||
|
|
||||||
|
interface ViewModel {
|
||||||
|
viewIcon: jotai.Atom<string>;
|
||||||
|
viewName: jotai.Atom<string>;
|
||||||
|
viewText: jotai.Atom<string>;
|
||||||
|
hasBackButton: jotai.Atom<boolean>;
|
||||||
|
hasForwardButton: jotai.Atom<boolean>;
|
||||||
|
hasSearch: jotai.Atom<boolean>;
|
||||||
|
|
||||||
|
onBack?: () => void;
|
||||||
|
onForward?: () => void;
|
||||||
|
onSearchChange?: (text: string) => void;
|
||||||
|
onSearch?: (text: string) => void;
|
||||||
|
getSettingsMenuItems?: () => ContextMenuItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// jotai doesn't export this type :/
|
||||||
|
type Loadable<T> = { state: "loading" } | { state: "hasData"; data: T } | { state: "hasError"; error: unknown };
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
@ -99,4 +99,68 @@ function fireAndForget(f: () => Promise<any>) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export { base64ToArray, base64ToString, fireAndForget, isBlank, jsonDeepEqual, makeIconClass, stringToBase64 };
|
const promiseWeakMap = new WeakMap<Promise<any>, ResolvedValue<any>>();
|
||||||
|
|
||||||
|
type ResolvedValue<T> = {
|
||||||
|
pending: boolean;
|
||||||
|
error: any;
|
||||||
|
value: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
// returns the value, pending state, and error of a promise
|
||||||
|
function getPromiseState<T>(promise: Promise<T>): [T, boolean, any] {
|
||||||
|
if (promise == null) {
|
||||||
|
return [null, false, null];
|
||||||
|
}
|
||||||
|
if (promiseWeakMap.has(promise)) {
|
||||||
|
const value = promiseWeakMap.get(promise);
|
||||||
|
return [value.value, value.pending, value.error];
|
||||||
|
}
|
||||||
|
const value: ResolvedValue<T> = {
|
||||||
|
pending: true,
|
||||||
|
error: null,
|
||||||
|
value: null,
|
||||||
|
};
|
||||||
|
promise.then(
|
||||||
|
(result) => {
|
||||||
|
value.pending = false;
|
||||||
|
value.error = null;
|
||||||
|
value.value = result;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
value.pending = false;
|
||||||
|
value.error = error;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
promiseWeakMap.set(promise, value);
|
||||||
|
return [value.value, value.pending, value.error];
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the value of a promise, or a default value if the promise is still pending (or had an error)
|
||||||
|
function getPromiseValue<T>(promise: Promise<T>, def: T): T {
|
||||||
|
const [value, pending, error] = getPromiseState(promise);
|
||||||
|
if (pending || error) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jotaiLoadableValue<T>(value: Loadable<T>, def: T): T {
|
||||||
|
if (value.state === "hasData") {
|
||||||
|
return value.data;
|
||||||
|
}
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
base64ToArray,
|
||||||
|
base64ToString,
|
||||||
|
fireAndForget,
|
||||||
|
getPromiseState,
|
||||||
|
getPromiseValue,
|
||||||
|
isBlank,
|
||||||
|
jotaiLoadableValue,
|
||||||
|
jsonDeepEqual,
|
||||||
|
makeIconClass,
|
||||||
|
stringToBase64,
|
||||||
|
};
|
||||||
|
@ -56,7 +56,7 @@ func getSettingsConfigDefaults() SettingsConfigType {
|
|||||||
"text/css": {Icon: "css3-alt fa-brands"},
|
"text/css": {Icon: "css3-alt fa-brands"},
|
||||||
"text/javascript": {Icon: "js fa-brands"},
|
"text/javascript": {Icon: "js fa-brands"},
|
||||||
"text/typescript": {Icon: "js fa-brands"},
|
"text/typescript": {Icon: "js fa-brands"},
|
||||||
"text/golang": {Icon: "go fa-brands"},
|
"text/golang": {Icon: "golang fa-brands"},
|
||||||
"text/html": {Icon: "html5 fa-brands"},
|
"text/html": {Icon: "html5 fa-brands"},
|
||||||
"text/less": {Icon: "less fa-brands"},
|
"text/less": {Icon: "less fa-brands"},
|
||||||
"text/markdown": {Icon: "markdown fa-brands"},
|
"text/markdown": {Icon: "markdown fa-brands"},
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"@/util/*": ["frontend/util/*"],
|
"@/util/*": ["frontend/util/*"],
|
||||||
"@/faraday/*": ["frontend/faraday/*"],
|
"@/faraday/*": ["frontend/faraday/*"],
|
||||||
"@/store/*": ["frontend/app/store/*"],
|
"@/store/*": ["frontend/app/store/*"],
|
||||||
|
"@/view/*": ["frontend/app/view/*"],
|
||||||
"@/element/*": ["frontend/app/element/*"],
|
"@/element/*": ["frontend/app/element/*"],
|
||||||
"@/bindings/*": ["frontend/bindings/github.com/wavetermdev/thenextwave/pkg/service/*"],
|
"@/bindings/*": ["frontend/bindings/github.com/wavetermdev/thenextwave/pkg/service/*"],
|
||||||
"@/gopkg/*": ["frontend/bindings/github.com/wavetermdev/thenextwave/pkg/*"],
|
"@/gopkg/*": ["frontend/bindings/github.com/wavetermdev/thenextwave/pkg/*"],
|
||||||
|
Loading…
Reference in New Issue
Block a user