From 94d6c14011272ef4f2b9a33682d75418305dc148 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 1 Aug 2024 15:35:13 -0700 Subject: [PATCH] reorganize block view more --- frontend/app/block/block.tsx | 509 +----------------------------- frontend/app/block/blockframe.tsx | 305 ++++++++++++++++++ frontend/app/block/blocktypes.ts | 31 ++ frontend/app/block/blockutil.tsx | 187 +++++++++++ frontend/app/tab/tabcontent.tsx | 3 +- 5 files changed, 529 insertions(+), 506 deletions(-) create mode 100644 frontend/app/block/blockframe.tsx create mode 100644 frontend/app/block/blocktypes.ts create mode 100644 frontend/app/block/blockutil.tsx diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index f3fffcd69..9df89b447 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -1,15 +1,12 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { useLongClick } from "@/app/hook/useLongClick"; +import { BlockComponentModel, BlockProps } from "@/app/block/blocktypes"; import { PlotView } from "@/app/view/plotview/plotview"; import { PreviewView, makePreviewModel } from "@/app/view/preview/preview"; -import { Button } from "@/element/button"; import { ErrorBoundary } from "@/element/errorboundary"; import { CenteredDiv } from "@/element/quickelems"; -import { ContextMenuModel } from "@/store/contextmenu"; -import { atoms, globalStore, setBlockFocus, useBlockAtom } from "@/store/global"; -import * as services from "@/store/services"; +import { atoms, setBlockFocus, useBlockAtom } from "@/store/global"; import * as WOS from "@/store/wos"; import * as util from "@/util/util"; import { CpuPlotView, makeCpuPlotViewModel } from "@/view/cpuplot/cpuplot"; @@ -17,511 +14,13 @@ import { HelpView } from "@/view/helpview/helpview"; import { TerminalView, makeTerminalModel } from "@/view/term/term"; import { WaveAi, makeWaveAiViewModel } from "@/view/waveai/waveai"; import { WebView, makeWebViewModel } from "@/view/webview/webview"; -import clsx from "clsx"; import * as jotai from "jotai"; import * as React from "react"; +import { BlockFrame } from "./blockframe"; +import { blockViewToIcon, blockViewToName } from "./blockutil"; -import { getLayoutStateAtomForTab } from "@/layout/lib/layoutAtom"; -import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; -import { isBlockMagnified } from "@/util/layoututil"; import "./block.less"; -export interface LayoutComponentModel { - disablePointerEvents: boolean; - onClose?: () => void; - onMagnifyToggle?: () => void; - dragHandleRef?: React.RefObject; -} - -interface BlockProps { - blockId: string; - preview: boolean; - layoutModel: LayoutComponentModel; -} - -interface BlockComponentModel { - onClick?: () => void; - onFocusCapture?: React.FocusEventHandler; - blockRef?: React.RefObject; -} - -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]+))$/; - -function processTitleString(titleString: string): React.ReactNode[] { - if (titleString == null) { - return null; - } - const tagRegex = /<(\/)?([a-z]+)(?::([#a-z0-9@-]+))?>/g; - let lastIdx = 0; - let match; - let partsStack = [[]]; - while ((match = tagRegex.exec(titleString)) != null) { - const lastPart = partsStack[partsStack.length - 1]; - const before = titleString.substring(lastIdx, match.index); - lastPart.push(before); - lastIdx = match.index + match[0].length; - const [_, isClosing, tagName, tagParam] = match; - if (tagName == "icon" && !isClosing) { - if (tagParam == null) { - continue; - } - const iconClass = util.makeIconClass(tagParam, false); - if (iconClass == null) { - continue; - } - lastPart.push(); - continue; - } - if (tagName == "c" || tagName == "color") { - if (isClosing) { - if (partsStack.length <= 1) { - continue; - } - partsStack.pop(); - continue; - } - if (tagParam == null) { - continue; - } - if (!tagParam.match(colorRegex)) { - continue; - } - let children = []; - const rtag = React.createElement("span", { key: match.index, style: { color: tagParam } }, children); - lastPart.push(rtag); - partsStack.push(children); - continue; - } - if (tagName == "i" || tagName == "b") { - if (isClosing) { - if (partsStack.length <= 1) { - continue; - } - partsStack.pop(); - continue; - } - let children = []; - const rtag = React.createElement(tagName, { key: match.index }, children); - lastPart.push(rtag); - partsStack.push(children); - continue; - } - } - partsStack[partsStack.length - 1].push(titleString.substring(lastIdx)); - return partsStack[0]; -} - -function getBlockHeaderIcon(blockIcon: string, blockData: Block): React.ReactNode { - let blockIconElem: React.ReactNode = null; - if (util.isBlank(blockIcon)) { - blockIcon = "square"; - } - let iconColor = blockData?.meta?.["icon:color"]; - if (iconColor && !iconColor.match(colorRegex)) { - iconColor = null; - } - let iconStyle = null; - if (!util.isBlank(iconColor)) { - iconStyle = { color: iconColor }; - } - const iconClass = util.makeIconClass(blockIcon, true); - if (iconClass != null) { - blockIconElem = ; - } - return blockIconElem; -} - -function getBlockHeaderText(blockIcon: string, blockData: Block, settings: SettingsConfigType): React.ReactNode { - if (!blockData) { - return "no block data"; - } - let blockIdStr = ""; - if (settings?.blockheader?.showblockids) { - blockIdStr = ` [${blockData.oid.substring(0, 8)}]`; - } - let blockIconElem = getBlockHeaderIcon(blockIcon, blockData); - if (!util.isBlank(blockData?.meta?.title)) { - try { - const rtn = processTitleString(blockData.meta.title) ?? []; - return [blockIconElem, ...rtn, blockIdStr == "" ? null : blockIdStr]; - } catch (e) { - console.error("error processing title", blockData.meta.title, e); - return [blockIconElem, blockData.meta.title + blockIdStr]; - } - } - let viewString = blockData?.meta?.view; - if (blockData?.meta?.controller == "cmd") { - viewString = "cmd"; - } - return [blockIconElem, viewString + blockIdStr]; -} - -function handleHeaderContextMenu( - e: React.MouseEvent, - blockData: Block, - viewModel: ViewModel, - onMagnifyToggle: () => void, - onClose: () => void -) { - e.preventDefault(); - e.stopPropagation(); - let menu: ContextMenuItem[] = [ - { - label: "Magnify Block", - click: () => { - onMagnifyToggle(); - }, - }, - { - label: "Move to New Window", - click: () => { - const currentTabId = globalStore.get(atoms.activeTabId); - try { - services.WindowService.MoveBlockToNewWindow(currentTabId, blockData.oid); - } catch (e) { - console.error("error moving block to new window", e); - } - }, - }, - { type: "separator" }, - { - label: "Copy BlockId", - click: () => { - navigator.clipboard.writeText(blockData.oid); - }, - }, - ]; - const extraItems = viewModel?.getSettingsMenuItems?.(); - if (extraItems && extraItems.length > 0) menu.push({ type: "separator" }, ...extraItems); - menu.push( - { type: "separator" }, - { - label: "Close Block", - click: onClose, - } - ); - ContextMenuModel.showContextMenu(menu, e); -} - -const IconButton = React.memo(({ decl, className }: { decl: HeaderIconButton; className?: string }) => { - const buttonRef = React.useRef(null); - useLongClick(buttonRef, decl.click, decl.longClick); - return ( -
- -
- ); -}); - -const Input = React.memo(({ decl, className }: { decl: HeaderInput; className: string }) => { - const { value, ref, isDisabled, onChange, onKeyDown, onFocus, onBlur } = decl; - return ( -
- onChange(e)} - onKeyDown={(e) => onKeyDown(e)} - onFocus={(e) => onFocus(e)} - onBlur={(e) => onBlur(e)} - /> -
- ); -}); - -const BlockFrame_Default_Component = ({ - blockId, - layoutModel, - viewModel, - blockModel, - preview, - numBlocksInTab, - children, -}: BlockFrameProps) => { - const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); - const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom); - const isFocusedAtom = useBlockAtom(blockId, "isFocused", () => { - return jotai.atom((get) => { - const winData = get(atoms.waveWindow); - return winData?.activeblockid === blockId; - }); - }); - let isFocused = jotai.useAtomValue(isFocusedAtom); - const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.meta?.view); - const viewName = util.useAtomValueSafe(viewModel.viewName) ?? blockViewToName(blockData?.meta?.view); - const headerTextUnion = util.useAtomValueSafe(viewModel.viewText); - const preIconButton = util.useAtomValueSafe(viewModel.preIconButton); - const endIconButtons = util.useAtomValueSafe(viewModel.endIconButtons); - const customBg = util.useAtomValueSafe(viewModel.blockBg); - const tabId = globalStore.get(atoms.activeTabId); - const tabAtom = WOS.getWaveObjectAtom(WOS.makeORef("tab", tabId)); - const layoutTreeState = util.useAtomValueSafe(getLayoutStateAtomForTab(tabId, tabAtom)); - if (preview) { - isFocused = true; - } - const 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"]; - } - let viewIconElem: JSX.Element = null; - if (viewIconUnion == null || typeof viewIconUnion === "string") { - const viewIcon = viewIconUnion as string; - viewIconElem =
{getBlockHeaderIcon(viewIcon, blockData)}
; - } else { - viewIconElem = ; - } - let preIconButtonElem: JSX.Element = null; - if (preIconButton) { - preIconButtonElem = ; - } - const endIconsElem: JSX.Element[] = []; - if (endIconButtons && endIconButtons.length > 0) { - endIconsElem.push( - ...endIconButtons.map((button, idx) => ( - - )) - ); - } - const settingsDecl: HeaderIconButton = { - elemtype: "iconbutton", - icon: "cog", - title: "Settings", - click: (e) => - handleHeaderContextMenu(e, blockData, viewModel, layoutModel?.onMagnifyToggle, layoutModel?.onClose), - }; - endIconsElem.push( - - ); - if (isBlockMagnified(layoutTreeState, blockId)) { - const magnifyDecl: HeaderIconButton = { - elemtype: "iconbutton", - icon: "regular@magnifying-glass-minus", - title: "Minimize", - click: layoutModel?.onMagnifyToggle, - }; - endIconsElem.push( - - ); - } - const closeDecl: HeaderIconButton = { - elemtype: "iconbutton", - icon: "xmark-large", - title: "Close", - click: layoutModel?.onClose, - }; - endIconsElem.push( - - ); - - function renderHeaderElements(headerTextUnion: HeaderElem[]): JSX.Element[] { - const headerTextElems: JSX.Element[] = []; - - function renderElement(elem: HeaderElem, key: number): JSX.Element { - if (elem.elemtype == "iconbutton") { - return ( - - ); - } else if (elem.elemtype == "input") { - return ; - } else if (elem.elemtype == "text") { - return ( -
- {elem.text} -
- ); - } else if (elem.elemtype == "textbutton") { - return ( - - ); - } else if (elem.elemtype == "div") { - return ( -
- {elem.children.map((child, childIdx) => renderElement(child, childIdx))} -
- ); - } - return null; - } - - for (let idx = 0; idx < headerTextUnion.length; idx++) { - const elem = headerTextUnion[idx]; - const renderedElement = renderElement(elem, idx); - if (renderedElement) { - headerTextElems.push(renderedElement); - } - } - - return headerTextElems; - } - - const headerTextElems: JSX.Element[] = []; - if (typeof headerTextUnion === "string") { - if (!util.isBlank(headerTextUnion)) { - headerTextElems.push( -
- {headerTextUnion} -
- ); - } - } else if (Array.isArray(headerTextUnion)) { - headerTextElems.push(...renderHeaderElements(headerTextUnion)); - } - - function handleDoubleClick() { - layoutModel?.onMagnifyToggle(); - } - - function handleKeyDown(e: React.KeyboardEvent) { - const waveEvent = adaptFromReactOrNativeKeyEvent(e); - if (checkKeyPressed(waveEvent, "Cmd:m")) { - e.preventDefault(); - layoutModel?.onMagnifyToggle(); - return; - } - } - - const innerStyle: React.CSSProperties = {}; - if (!preview && customBg?.bg != null) { - innerStyle.background = customBg.bg; - if (customBg["bg:opacity"] != null) { - innerStyle.opacity = customBg["bg:opacity"]; - } - if (customBg["bg:blendmode"] != null) { - innerStyle.backgroundBlendMode = customBg["bg:blendmode"]; - } - } - const previewElem =
{viewIconElem}
; - return ( -
-
-
-
- handleHeaderContextMenu( - e, - blockData, - viewModel, - layoutModel?.onMagnifyToggle, - layoutModel?.onClose - ) - } - > - {preIconButtonElem} -
- {viewIconElem} -
{viewName}
- {settingsConfig?.blockheader?.showblockids && ( -
[{blockId.substring(0, 8)}]
- )} -
-
{headerTextElems}
-
{endIconsElem}
-
- {preview ? previewElem : children} -
-
- ); -}; - -const BlockFrame_Default = React.memo(BlockFrame_Default_Component) as typeof BlockFrame_Default_Component; - -const BlockFrame = React.memo((props: BlockFrameProps) => { - const blockId = props.blockId; - const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); - const tabData = jotai.useAtomValue(atoms.tabAtom); - - if (!blockId || !blockData) { - return null; - } - let FrameElem = BlockFrame_Default; - const numBlocks = tabData?.blockids?.length ?? 0; - return ; -}); - -function blockViewToIcon(view: string): string { - if (view == "term") { - return "terminal"; - } - if (view == "preview") { - return "file"; - } - if (view == "web") { - return "globe"; - } - if (view == "waveai") { - return "sparkles"; - } - if (view == "help") { - return "circle-question"; - } - return "square"; -} - -function blockViewToName(view: string): string { - if (util.isBlank(view)) { - return "(No View)"; - } - if (view == "term") { - return "Terminal"; - } - if (view == "preview") { - return "Preview"; - } - if (view == "web") { - return "Web"; - } - if (view == "waveai") { - return "WaveAI"; - } - if (view == "help") { - return "Help"; - } - return view; -} - function getViewElemAndModel( blockId: string, blockView: string, diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx new file mode 100644 index 000000000..83329d9f8 --- /dev/null +++ b/frontend/app/block/blockframe.tsx @@ -0,0 +1,305 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { blockViewToIcon, blockViewToName, getBlockHeaderIcon, IconButton, Input } from "@/app/block/blockutil"; +import { Button } from "@/app/element/button"; +import { ContextMenuModel } from "@/app/store/contextmenu"; +import { atoms, globalStore, useBlockAtom, WOS } from "@/app/store/global"; +import * as services from "@/app/store/services"; +import { getLayoutStateAtomForTab } from "@/layout/lib/layoutAtom"; +import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; +import { isBlockMagnified } from "@/util/layoututil"; +import * as util from "@/util/util"; +import clsx from "clsx"; +import * as jotai from "jotai"; +import * as React from "react"; +import { BlockFrameProps } from "./blocktypes"; + +function handleHeaderContextMenu( + e: React.MouseEvent, + blockData: Block, + viewModel: ViewModel, + onMagnifyToggle: () => void, + onClose: () => void +) { + e.preventDefault(); + e.stopPropagation(); + let menu: ContextMenuItem[] = [ + { + label: "Magnify Block", + click: () => { + onMagnifyToggle(); + }, + }, + { + label: "Move to New Window", + click: () => { + const currentTabId = globalStore.get(atoms.activeTabId); + try { + services.WindowService.MoveBlockToNewWindow(currentTabId, blockData.oid); + } catch (e) { + console.error("error moving block to new window", e); + } + }, + }, + { type: "separator" }, + { + label: "Copy BlockId", + click: () => { + navigator.clipboard.writeText(blockData.oid); + }, + }, + ]; + const extraItems = viewModel?.getSettingsMenuItems?.(); + if (extraItems && extraItems.length > 0) menu.push({ type: "separator" }, ...extraItems); + menu.push( + { type: "separator" }, + { + label: "Close Block", + click: onClose, + } + ); + ContextMenuModel.showContextMenu(menu, e); +} + +const BlockFrame_Default_Component = ({ + blockId, + layoutModel, + viewModel, + blockModel, + preview, + numBlocksInTab, + children, +}: BlockFrameProps) => { + const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); + const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom); + const isFocusedAtom = useBlockAtom(blockId, "isFocused", () => { + return jotai.atom((get) => { + const winData = get(atoms.waveWindow); + return winData?.activeblockid === blockId; + }); + }); + let isFocused = jotai.useAtomValue(isFocusedAtom); + const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.meta?.view); + const viewName = util.useAtomValueSafe(viewModel.viewName) ?? blockViewToName(blockData?.meta?.view); + const headerTextUnion = util.useAtomValueSafe(viewModel.viewText); + const preIconButton = util.useAtomValueSafe(viewModel.preIconButton); + const endIconButtons = util.useAtomValueSafe(viewModel.endIconButtons); + const customBg = util.useAtomValueSafe(viewModel.blockBg); + const tabId = globalStore.get(atoms.activeTabId); + const tabAtom = WOS.getWaveObjectAtom(WOS.makeORef("tab", tabId)); + const layoutTreeState = util.useAtomValueSafe(getLayoutStateAtomForTab(tabId, tabAtom)); + if (preview) { + isFocused = true; + } + const 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"]; + } + let viewIconElem: JSX.Element = null; + if (viewIconUnion == null || typeof viewIconUnion === "string") { + const viewIcon = viewIconUnion as string; + viewIconElem =
{getBlockHeaderIcon(viewIcon, blockData)}
; + } else { + viewIconElem = ; + } + let preIconButtonElem: JSX.Element = null; + if (preIconButton) { + preIconButtonElem = ; + } + const endIconsElem: JSX.Element[] = []; + if (endIconButtons && endIconButtons.length > 0) { + endIconsElem.push( + ...endIconButtons.map((button, idx) => ( + + )) + ); + } + const settingsDecl: HeaderIconButton = { + elemtype: "iconbutton", + icon: "cog", + title: "Settings", + click: (e) => + handleHeaderContextMenu(e, blockData, viewModel, layoutModel?.onMagnifyToggle, layoutModel?.onClose), + }; + endIconsElem.push( + + ); + if (isBlockMagnified(layoutTreeState, blockId)) { + const magnifyDecl: HeaderIconButton = { + elemtype: "iconbutton", + icon: "regular@magnifying-glass-minus", + title: "Minimize", + click: layoutModel?.onMagnifyToggle, + }; + endIconsElem.push( + + ); + } + const closeDecl: HeaderIconButton = { + elemtype: "iconbutton", + icon: "xmark-large", + title: "Close", + click: layoutModel?.onClose, + }; + endIconsElem.push( + + ); + + function renderHeaderElements(headerTextUnion: HeaderElem[]): JSX.Element[] { + const headerTextElems: JSX.Element[] = []; + + function renderElement(elem: HeaderElem, key: number): JSX.Element { + if (elem.elemtype == "iconbutton") { + return ( + + ); + } else if (elem.elemtype == "input") { + return ; + } else if (elem.elemtype == "text") { + return ( +
+ {elem.text} +
+ ); + } else if (elem.elemtype == "textbutton") { + return ( + + ); + } else if (elem.elemtype == "div") { + return ( +
+ {elem.children.map((child, childIdx) => renderElement(child, childIdx))} +
+ ); + } + return null; + } + + for (let idx = 0; idx < headerTextUnion.length; idx++) { + const elem = headerTextUnion[idx]; + const renderedElement = renderElement(elem, idx); + if (renderedElement) { + headerTextElems.push(renderedElement); + } + } + + return headerTextElems; + } + + const headerTextElems: JSX.Element[] = []; + if (typeof headerTextUnion === "string") { + if (!util.isBlank(headerTextUnion)) { + headerTextElems.push( +
+ {headerTextUnion} +
+ ); + } + } else if (Array.isArray(headerTextUnion)) { + headerTextElems.push(...renderHeaderElements(headerTextUnion)); + } + + function handleDoubleClick() { + layoutModel?.onMagnifyToggle(); + } + + function handleKeyDown(e: React.KeyboardEvent) { + const waveEvent = adaptFromReactOrNativeKeyEvent(e); + if (checkKeyPressed(waveEvent, "Cmd:m")) { + e.preventDefault(); + layoutModel?.onMagnifyToggle(); + return; + } + } + + const innerStyle: React.CSSProperties = {}; + if (!preview && customBg?.bg != null) { + innerStyle.background = customBg.bg; + if (customBg["bg:opacity"] != null) { + innerStyle.opacity = customBg["bg:opacity"]; + } + if (customBg["bg:blendmode"] != null) { + innerStyle.backgroundBlendMode = customBg["bg:blendmode"]; + } + } + const previewElem =
{viewIconElem}
; + return ( +
+
+
+
+ handleHeaderContextMenu( + e, + blockData, + viewModel, + layoutModel?.onMagnifyToggle, + layoutModel?.onClose + ) + } + > + {preIconButtonElem} +
+ {viewIconElem} +
{viewName}
+ {settingsConfig?.blockheader?.showblockids && ( +
[{blockId.substring(0, 8)}]
+ )} +
+
{headerTextElems}
+
{endIconsElem}
+
+ {preview ? previewElem : children} +
+
+ ); +}; + +const BlockFrame_Default = React.memo(BlockFrame_Default_Component) as typeof BlockFrame_Default_Component; + +const BlockFrame = React.memo((props: BlockFrameProps) => { + const blockId = props.blockId; + const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); + const tabData = jotai.useAtomValue(atoms.tabAtom); + + if (!blockId || !blockData) { + return null; + } + let FrameElem = BlockFrame_Default; + const numBlocks = tabData?.blockids?.length ?? 0; + return ; +}); + +export { BlockFrame }; diff --git a/frontend/app/block/blocktypes.ts b/frontend/app/block/blocktypes.ts new file mode 100644 index 000000000..e638462d0 --- /dev/null +++ b/frontend/app/block/blocktypes.ts @@ -0,0 +1,31 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +export interface LayoutComponentModel { + disablePointerEvents: boolean; + onClose?: () => void; + onMagnifyToggle?: () => void; + dragHandleRef?: React.RefObject; +} + +export interface BlockProps { + blockId: string; + preview: boolean; + layoutModel: LayoutComponentModel; +} + +export interface BlockComponentModel { + onClick?: () => void; + onFocusCapture?: React.FocusEventHandler; + blockRef?: React.RefObject; +} + +export interface BlockFrameProps { + blockId: string; + blockModel?: BlockComponentModel; + layoutModel?: LayoutComponentModel; + viewModel?: ViewModel; + preview: boolean; + numBlocksInTab?: number; + children?: React.ReactNode; +} diff --git a/frontend/app/block/blockutil.tsx b/frontend/app/block/blockutil.tsx new file mode 100644 index 000000000..1aa8d4c24 --- /dev/null +++ b/frontend/app/block/blockutil.tsx @@ -0,0 +1,187 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { useLongClick } from "@/app/hook/useLongClick"; +import * as util from "@/util/util"; +import clsx from "clsx"; +import * as React from "react"; + +export const colorRegex = /^((#[0-9a-f]{6,8})|([a-z]+))$/; + +export function blockViewToIcon(view: string): string { + if (view == "term") { + return "terminal"; + } + if (view == "preview") { + return "file"; + } + if (view == "web") { + return "globe"; + } + if (view == "waveai") { + return "sparkles"; + } + if (view == "help") { + return "circle-question"; + } + return "square"; +} + +export function blockViewToName(view: string): string { + if (util.isBlank(view)) { + return "(No View)"; + } + if (view == "term") { + return "Terminal"; + } + if (view == "preview") { + return "Preview"; + } + if (view == "web") { + return "Web"; + } + if (view == "waveai") { + return "WaveAI"; + } + if (view == "help") { + return "Help"; + } + return view; +} + +export function processTitleString(titleString: string): React.ReactNode[] { + if (titleString == null) { + return null; + } + const tagRegex = /<(\/)?([a-z]+)(?::([#a-z0-9@-]+))?>/g; + let lastIdx = 0; + let match; + let partsStack = [[]]; + while ((match = tagRegex.exec(titleString)) != null) { + const lastPart = partsStack[partsStack.length - 1]; + const before = titleString.substring(lastIdx, match.index); + lastPart.push(before); + lastIdx = match.index + match[0].length; + const [_, isClosing, tagName, tagParam] = match; + if (tagName == "icon" && !isClosing) { + if (tagParam == null) { + continue; + } + const iconClass = util.makeIconClass(tagParam, false); + if (iconClass == null) { + continue; + } + lastPart.push(); + continue; + } + if (tagName == "c" || tagName == "color") { + if (isClosing) { + if (partsStack.length <= 1) { + continue; + } + partsStack.pop(); + continue; + } + if (tagParam == null) { + continue; + } + if (!tagParam.match(colorRegex)) { + continue; + } + let children = []; + const rtag = React.createElement("span", { key: match.index, style: { color: tagParam } }, children); + lastPart.push(rtag); + partsStack.push(children); + continue; + } + if (tagName == "i" || tagName == "b") { + if (isClosing) { + if (partsStack.length <= 1) { + continue; + } + partsStack.pop(); + continue; + } + let children = []; + const rtag = React.createElement(tagName, { key: match.index }, children); + lastPart.push(rtag); + partsStack.push(children); + continue; + } + } + partsStack[partsStack.length - 1].push(titleString.substring(lastIdx)); + return partsStack[0]; +} + +export function getBlockHeaderIcon(blockIcon: string, blockData: Block): React.ReactNode { + let blockIconElem: React.ReactNode = null; + if (util.isBlank(blockIcon)) { + blockIcon = "square"; + } + let iconColor = blockData?.meta?.["icon:color"]; + if (iconColor && !iconColor.match(colorRegex)) { + iconColor = null; + } + let iconStyle = null; + if (!util.isBlank(iconColor)) { + iconStyle = { color: iconColor }; + } + const iconClass = util.makeIconClass(blockIcon, true); + if (iconClass != null) { + blockIconElem = ; + } + return blockIconElem; +} + +export function getBlockHeaderText(blockIcon: string, blockData: Block, settings: SettingsConfigType): React.ReactNode { + if (!blockData) { + return "no block data"; + } + let blockIdStr = ""; + if (settings?.blockheader?.showblockids) { + blockIdStr = ` [${blockData.oid.substring(0, 8)}]`; + } + let blockIconElem = getBlockHeaderIcon(blockIcon, blockData); + if (!util.isBlank(blockData?.meta?.title)) { + try { + const rtn = processTitleString(blockData.meta.title) ?? []; + return [blockIconElem, ...rtn, blockIdStr == "" ? null : blockIdStr]; + } catch (e) { + console.error("error processing title", blockData.meta.title, e); + return [blockIconElem, blockData.meta.title + blockIdStr]; + } + } + let viewString = blockData?.meta?.view; + if (blockData?.meta?.controller == "cmd") { + viewString = "cmd"; + } + return [blockIconElem, viewString + blockIdStr]; +} + +export const IconButton = React.memo(({ decl, className }: { decl: HeaderIconButton; className?: string }) => { + const buttonRef = React.useRef(null); + useLongClick(buttonRef, decl.click, decl.longClick); + return ( +
+ +
+ ); +}); + +export const Input = React.memo(({ decl, className }: { decl: HeaderInput; className: string }) => { + const { value, ref, isDisabled, onChange, onKeyDown, onFocus, onBlur } = decl; + return ( +
+ onChange(e)} + onKeyDown={(e) => onKeyDown(e)} + onFocus={(e) => onFocus(e)} + onBlur={(e) => onBlur(e)} + /> +
+ ); +}); diff --git a/frontend/app/tab/tabcontent.tsx b/frontend/app/tab/tabcontent.tsx index b4b48d766..e73093c9a 100644 --- a/frontend/app/tab/tabcontent.tsx +++ b/frontend/app/tab/tabcontent.tsx @@ -1,7 +1,8 @@ // Copyright 2023, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { Block, LayoutComponentModel } from "@/app/block/block"; +import { Block } from "@/app/block/block"; +import { LayoutComponentModel } from "@/app/block/blocktypes"; import { getApi } from "@/store/global"; import * as services from "@/store/services"; import * as WOS from "@/store/wos";