Compare commits

..

19 Commits

Author SHA1 Message Date
wave-builder[bot] 8423334cde chore: bump package version to 0.8.0-beta.14 2024-09-20 19:51:59 +00:00
Sylvie Crowe bc057876a1 Powershell Execution Policy (#798)
Bypasses the Execution Policy for setting the script that adds wsh to
the path.
2024-09-20 12:48:29 -07:00
Mike Sawka d344fd9b6d fix cpu plot timing issue (#799) 2024-09-20 12:47:10 -07:00
Mike Sawka 31414a7536 don't open files when stating directory (#797) 2024-09-20 12:25:46 -07:00
Mike Sawka e281cebb56 tips update (add to starter layout) (#796) 2024-09-20 12:17:49 -07:00
Mike Sawka 65c446e4a1 wash meta.url through url search (#795) 2024-09-20 11:34:04 -07:00
Mike Sawka 047513bf3a default url, default search, open in external browser (#794) 2024-09-20 11:24:37 -07:00
Evan Simkowitz 78f838439a prevent running this workflow on unprotected branches 2024-09-20 09:58:35 -07:00
wave-builder[bot] 470dfb19b7 chore: bump package version to 0.8.0-beta.13 2024-09-20 16:51:52 +00:00
sawka ade5bc01b3 bump version to 0.8.0-beta.12 2024-09-19 22:14:53 -07:00
sawka c768616a24 only set executableName for linux 2024-09-19 22:14:14 -07:00
sawka bb7ea131b4 beta11 2024-09-19 21:55:56 -07:00
sawka 0b2fdf0ca0 update app name in permissions part as well 2024-09-19 21:51:24 -07:00
sawka eced7120c7 override name for linux 2024-09-19 21:49:10 -07:00
sawka 4395c7410d beta10 2024-09-19 21:38:53 -07:00
sawka b0c5443916 use productName for executable 2024-09-19 21:38:10 -07:00
sawka 85ec6d45fe beta9 2024-09-19 20:02:41 -07:00
sawka fd97f895d0 bump version 2024-09-19 18:44:59 -07:00
Sylvie Crowe 071a051382 Remember Markdown Changes (#790)
This addresses issues of saved markdown changes not showing up when
switching to the rendered preview. It has some limitations as it does
not preserve the state when changing tabs or refreshing the app.
2024-09-19 18:32:36 -07:00
25 changed files with 326 additions and 1160 deletions
+1
View File
@@ -58,6 +58,7 @@ jobs:
shell: bash
- name: "Push version bump: ${{ steps.bump-version.outputs.WAVETERM_VERSION }}"
if: github.ref_protected
run: |
# Create a new commit for the package version bump in package.json
export VERSION=${{ steps.bump-version.outputs.WAVETERM_VERSION }}
-1
View File
@@ -1,7 +1,6 @@
body {
height: 100vh;
padding: 0;
overflow: auto;
}
#storybook-root {
-3
View File
@@ -3,9 +3,6 @@ import type { Preview } from "@storybook/react";
import React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import "../frontend/app/theme.less";
import "../frontend/app/app.less";
import "../frontend/app/reset.less";
import "./global.css";
const preview: Preview = {
+3 -2
View File
@@ -10,7 +10,7 @@ const path = require("path");
const config = {
appId: pkg.build.appId,
productName: pkg.productName,
executableName: pkg.name,
executableName: pkg.productName,
artifactName: "${productName}-${platform}-${arch}-${version}.${ext}",
generateUpdatesFilesForAllChannels: true,
npmRebuild: false,
@@ -55,6 +55,7 @@ const config = {
linux: {
artifactName: "${name}-${platform}-${arch}-${version}.${ext}",
category: "TerminalEmulator",
executableName: pkg.name,
icon: "build/icons.icns",
target: ["zip", "deb", "rpm", "AppImage", "pacman"],
synopsis: pkg.description,
@@ -86,7 +87,7 @@ const config = {
if (context.electronPlatformName === "darwin" && context.arch === Arch.universal) {
const packageBinDir = path.resolve(
context.appOutDir,
`${pkg.name}.app/Contents/Resources/app.asar.unpacked/dist/bin`
`${pkg.productName}.app/Contents/Resources/app.asar.unpacked/dist/bin`
);
// Reapply file permissions to the wavesrv binaries in the final app package
-121
View File
@@ -1,121 +0,0 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";
import { Button } from "./button";
const meta = {
title: "Elements/Button",
component: Button,
args: {
children: "Click Me",
disabled: false,
className: "",
onClick: fn(),
},
argTypes: {
onClick: {
action: "clicked",
description: "Click event handler",
},
children: {
description: "Content inside the button",
},
disabled: {
description: "Disables the button if true",
},
className: {
description: "Additional class names to style the button",
},
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Disabled: Story = {
args: {
disabled: true,
children: "Disabled Button",
},
};
export const GreySolid: Story = {
args: {
className: "solid grey",
children: "Grey Solid Button",
},
};
export const RedSolid: Story = {
args: {
className: "solid red",
children: "Red Solid Button",
},
};
export const YellowSolid: Story = {
args: {
className: "solid yellow",
children: "Yellow Solid Button",
},
};
export const GreenOutlined: Story = {
args: {
className: "outlined green",
children: "Green Outline Button",
},
};
export const GreyOutlined: Story = {
args: {
className: "outlined grey",
children: "Grey Outline Button",
},
};
export const RedOutlined: Story = {
args: {
className: "outlined red",
children: "Red Outline Button",
},
};
export const YellowOutlined: Story = {
args: {
className: "outlined yellow",
children: "Yellow Outline Button",
},
};
export const GreenGhostText: Story = {
args: {
className: "ghost green",
children: "Yellow Ghost Text Button",
},
};
export const GreyGhostText: Story = {
args: {
className: "ghost grey",
children: "Grey Ghost Text Button",
},
};
export const RedGhost: Story = {
args: {
className: "ghost red",
children: "Red Ghost Text Button",
},
};
export const YellowGhostText: Story = {
args: {
className: "ghost yellow",
children: "Yellow Ghost Text Button",
},
};
+28 -26
View File
@@ -9,39 +9,41 @@ import "./button.less";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
className?: string;
children?: ReactNode;
target?: string;
source?: string;
}
const Button = memo(
forwardRef<HTMLButtonElement, ButtonProps>(({ children, disabled, className = "", ...props }: ButtonProps, ref) => {
const btnRef = useRef<HTMLButtonElement>(null);
useImperativeHandle(ref, () => btnRef.current as HTMLButtonElement);
forwardRef<HTMLButtonElement, ButtonProps>(
({ children, disabled, source, className = "", ...props }: ButtonProps, ref) => {
const btnRef = useRef<HTMLButtonElement>(null);
useImperativeHandle(ref, () => btnRef.current as HTMLButtonElement);
const childrenArray = Children.toArray(children);
const childrenArray = Children.toArray(children);
// Check if the className contains any of the categories: solid, outlined, or ghost
const containsButtonCategory = /(solid|outline|ghost)/.test(className);
// If no category is present, default to 'solid'
const categoryClassName = containsButtonCategory ? className : `solid ${className}`;
// Check if the className contains any of the categories: solid, outlined, or ghost
const containsButtonCategory = /(solid|outline|ghost)/.test(className);
// If no category is present, default to 'solid'
const categoryClassName = containsButtonCategory ? className : `solid ${className}`;
// Check if the className contains any of the color options: green, grey, red, or yellow
const containsColor = /(green|grey|red|yellow)/.test(categoryClassName);
// If no color is present, default to 'green'
const finalClassName = containsColor ? categoryClassName : `green ${categoryClassName}`;
// Check if the className contains any of the color options: green, grey, red, or yellow
const containsColor = /(green|grey|red|yellow)/.test(categoryClassName);
// If no color is present, default to 'green'
const finalClassName = containsColor ? categoryClassName : `green ${categoryClassName}`;
return (
<button
ref={btnRef}
tabIndex={disabled ? -1 : 0}
className={clsx("button", finalClassName)}
disabled={disabled}
{...props}
>
{childrenArray}
</button>
);
})
return (
<button
ref={btnRef}
tabIndex={disabled ? -1 : 0}
className={clsx("button", finalClassName)}
disabled={disabled}
{...props}
>
{childrenArray}
</button>
);
}
)
);
Button.displayName = "Button";
export { Button };
-40
View File
@@ -1,40 +0,0 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
.dropdown {
position: absolute;
z-index: 1000; // TODO: put this in theme.less
display: flex;
width: 125px;
padding: 2px;
flex-direction: column;
justify-content: flex-end;
align-items: flex-start;
gap: 1px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.15);
background: #212121;
box-shadow: 0px 8px 24px 0px rgba(0, 0, 0, 0.3);
.dropdown-item {
padding: 4px 6px;
cursor: pointer;
color: #fff;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: -0.12px;
width: 100%;
border-radius: 2px;
&:hover,
&.active {
background-color: var(--accent-color);
}
.arrow {
float: right;
}
}
}
-301
View File
@@ -1,301 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";
import { useRef, useState } from "react";
import { Dropdown } from "./dropdown";
const meta = {
title: "Elements/Dropdown",
component: Dropdown,
args: {
items: [],
anchorRef: undefined,
boundaryRef: undefined,
className: "",
setVisibility: fn(),
},
argTypes: {
items: {
description: "Items of dropdown",
},
anchorRef: {
description: "Element to attach the dropdown",
},
setVisibility: {
description: "Visibility event handler",
},
boundaryRef: {
description: "Component that defines the boundaries of the dropdown",
},
className: {
description: "Custom className",
},
},
} satisfies Meta<typeof Dropdown>;
export default meta;
type Story = StoryObj<typeof meta>;
export const DefaultRender: Story = {
render: (args) => {
const anchorRef = useRef<HTMLDivElement>(null);
const boundaryRef = useRef<HTMLDivElement>(null);
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const handleAnchorClick = () => {
setIsDropdownVisible((prev) => !prev);
};
const mapItemsWithClick = (items: any[]) => {
return items.map((item) => ({
...item,
onClick: () => {
// Call the original onClick if it exists
if (item.onClick) {
item.onClick();
}
// Close the dropdown after an item is clicked
setIsDropdownVisible(false);
},
// Recursively update subItems' onClick handlers
subItems: item.subItems ? mapItemsWithClick(item.subItems) : undefined,
}));
};
// Modify args to include updated items with the new onClick behavior
const modifiedArgs = {
...args,
items: mapItemsWithClick(args.items),
};
return (
<div
ref={boundaryRef}
className="boundary"
style={{ padding: "20px", height: "300px", border: "2px solid black" }}
>
<div style={{ height: "400px" }}>
<div
ref={anchorRef}
style={{
backgroundColor: "lightblue",
padding: "10px",
display: "inline-block",
cursor: "pointer",
}}
onClick={handleAnchorClick}
>
Anchor Element
</div>
</div>
{isDropdownVisible && (
<Dropdown
{...modifiedArgs}
setVisibility={(visible) => setIsDropdownVisible(visible)}
anchorRef={anchorRef}
boundaryRef={boundaryRef}
/>
)}
</div>
);
},
args: {
items: [
{ label: "Option 1", onClick: (e) => console.log("Clicked Option 1") },
{
label: "Option 2",
onClick: (e) => console.log("Clicked Option 2"),
subItems: [
{ label: "Option 2 -> 1", onClick: (e) => console.log("Clicked Option 2 -> 1") },
{ label: "Option 2 -> 2", onClick: (e) => console.log("Clicked Option 2 -> 2") },
],
},
{
label: "Option 3",
onClick: (e) => console.log("Clicked Option 3"),
subItems: [
{ label: "Option 3 -> 1", onClick: (e) => console.log("Clicked Option 3 -> 1") },
{ label: "Option 3 -> 2", onClick: (e) => console.log("Clicked Option 3 -> 2") },
{
label: "Option 3 -> 3",
onClick: (e) => console.log("Clicked Option 3 -> 3"),
subItems: [
{ label: "Option 3 -> 3 -> 1", onClick: (e) => console.log("Clicked Option 3 -> 3 -> 1") },
{ label: "Option 3 -> 3 -> 2", onClick: (e) => console.log("Clicked Option 3 -> 3 -> 2") },
{ label: "Option 3 -> 3 -> 3", onClick: (e) => console.log("Clicked Option 3 -> 3 -> 3") },
{
label: "Option 3 -> 3 -> 4",
onClick: (e) => console.log("Clicked Option 3 -> 3 -> 4"),
subItems: [
{
label: "Option 3 -> 3 -> 4 -> 1",
onClick: (e) => console.log("Clicked Option 3 -> 3 -> 4 -> 1"),
},
{
label: "Option 3 -> 3 -> 4 -> 2",
onClick: (e) => console.log("Clicked Option 3 -> 3 -> 4 -> 2"),
},
{
label: "Option 3 -> 3 -> 4 -> 3",
onClick: (e) => console.log("Clicked Option 3 -> 3 -> 4 -> 3"),
},
],
},
],
},
],
},
{
label: "Option 4",
onClick: (e) => console.log("Clicked Option 4"),
subItems: [
{ label: "Option 4 -> 1", onClick: (e) => console.log("Clicked Option 4 -> 1") },
{ label: "Option 4 -> 2", onClick: (e) => console.log("Clicked Option 4 -> 2") },
{ label: "Option 4 -> 3", onClick: (e) => console.log("Clicked Option 4 -> 3") },
{ label: "Option 4 -> 4", onClick: (e) => console.log("Clicked Option 4 -> 4") },
{ label: "Option 4 -> 5", onClick: (e) => console.log("Clicked Option 4 -> 5") },
{ label: "Option 4 -> 6", onClick: (e) => console.log("Clicked Option 4 -> 6") },
{ label: "Option 4 -> 7", onClick: (e) => console.log("Clicked Option 4 -> 7") },
],
},
],
},
};
export const CustomRender: Story = {
render: (args) => {
const anchorRef = useRef<HTMLDivElement>(null);
const boundaryRef = useRef<HTMLDivElement>(null);
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const handleAnchorClick = () => {
setIsDropdownVisible((prev) => !prev);
};
const mapItemsWithClick = (items: any[]) => {
return items.map((item) => ({
...item,
onClick: () => {
// Call the original onClick if it exists
if (item.onClick) {
item.onClick();
}
// Close the dropdown after an item is clicked
setIsDropdownVisible(false);
},
// Recursively update subItems' onClick handlers
subItems: item.subItems ? mapItemsWithClick(item.subItems) : undefined,
}));
};
// Custom render function for menu items
const renderMenuItem = (item: any, props: any) => (
<div {...props}>
<strong>{item.label}</strong>
{item.subItems && <span style={{ marginLeft: "10px", color: "#888" }}>â–¶</span>}
</div>
);
// Custom render function for the entire menu
const renderMenu = (subMenu: JSX.Element) => <div>{subMenu}</div>;
// Modify args to include updated items with the new onClick behavior
const modifiedArgs = {
...args,
items: mapItemsWithClick(args.items),
};
return (
<div
ref={boundaryRef}
className="boundary"
style={{ padding: "20px", height: "300px", border: "2px solid black" }}
>
<div style={{ height: "400px" }}>
<div
ref={anchorRef}
style={{
backgroundColor: "lightblue",
padding: "10px",
display: "inline-block",
cursor: "pointer",
}}
onClick={handleAnchorClick}
>
Anchor Element
</div>
</div>
{isDropdownVisible && (
<Dropdown
{...modifiedArgs}
setVisibility={(visible) => setIsDropdownVisible(visible)}
anchorRef={anchorRef}
boundaryRef={boundaryRef}
renderMenu={renderMenu}
renderMenuItem={renderMenuItem}
/>
)}
</div>
);
},
args: {
items: [
{ label: "Option 1", onClick: (e) => console.log("Clicked Option 1") },
{
label: "Option 2",
onClick: (e) => console.log("Clicked Option 2"),
subItems: [
{ label: "Option 2 -> 1", onClick: (e) => console.log("Clicked Option 2 -> 1") },
{ label: "Option 2 -> 2", onClick: (e) => console.log("Clicked Option 2 -> 2") },
],
},
{
label: "Option 3",
onClick: (e) => console.log("Clicked Option 3"),
subItems: [
{ label: "Option 3 -> 1", onClick: (e) => console.log("Clicked Option 3 -> 1") },
{ label: "Option 3 -> 2", onClick: (e) => console.log("Clicked Option 3 -> 2") },
{
label: "Option 3 -> 3",
onClick: (e) => console.log("Clicked Option 3 -> 3"),
subItems: [
{ label: "Option 3 -> 3 -> 1", onClick: (e) => console.log("Clicked Option 3 -> 3 -> 1") },
{ label: "Option 3 -> 3 -> 2", onClick: (e) => console.log("Clicked Option 3 -> 3 -> 2") },
{ label: "Option 3 -> 3 -> 3", onClick: (e) => console.log("Clicked Option 3 -> 3 -> 3") },
{
label: "Option 3 -> 3 -> 4",
onClick: (e) => console.log("Clicked Option 3 -> 3 -> 4"),
subItems: [
{
label: "Option 3 -> 3 -> 4 -> 1",
onClick: (e) => console.log("Clicked Option 3 -> 3 -> 4 -> 1"),
},
{
label: "Option 3 -> 3 -> 4 -> 2",
onClick: (e) => console.log("Clicked Option 3 -> 3 -> 4 -> 2"),
},
{
label: "Option 3 -> 3 -> 4 -> 3",
onClick: (e) => console.log("Clicked Option 3 -> 3 -> 4 -> 3"),
},
],
},
],
},
],
},
{
label: "Option 4",
onClick: (e) => console.log("Clicked Option 4"),
subItems: [
{ label: "Option 4 -> 1", onClick: (e) => console.log("Clicked Option 4 -> 1") },
{ label: "Option 4 -> 2", onClick: (e) => console.log("Clicked Option 4 -> 2") },
{ label: "Option 4 -> 3", onClick: (e) => console.log("Clicked Option 4 -> 3") },
{ label: "Option 4 -> 4", onClick: (e) => console.log("Clicked Option 4 -> 4") },
{ label: "Option 4 -> 5", onClick: (e) => console.log("Clicked Option 4 -> 5") },
{ label: "Option 4 -> 6", onClick: (e) => console.log("Clicked Option 4 -> 6") },
{ label: "Option 4 -> 7", onClick: (e) => console.log("Clicked Option 4 -> 7") },
],
},
],
},
};
-351
View File
@@ -1,351 +0,0 @@
import { useHeight } from "@/app/hook/useHeight";
import { useWidth } from "@/app/hook/useWidth";
import clsx from "clsx";
import React, { memo, useEffect, useLayoutEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
import "./dropdown.less";
type DropdownItem = {
label: string;
subItems?: DropdownItem[];
onClick?: (e) => void;
};
const SubMenu = memo(
({
subItems,
parentKey,
subMenuPosition,
visibleSubMenus,
hoveredItems,
subMenuRefs,
handleMouseEnterItem,
handleOnClick,
renderMenu,
renderMenuItem,
}: {
subItems: DropdownItem[];
parentKey: string;
subMenuPosition: {
[key: string]: { top: number; left: number; label: string };
};
visibleSubMenus: { [key: string]: any };
hoveredItems: string[];
subMenuRefs: React.MutableRefObject<{ [key: string]: React.RefObject<HTMLDivElement> }>;
handleMouseEnterItem: (
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
parentKey: string | null,
index: number,
item: DropdownItem
) => void;
handleOnClick: (e: React.MouseEvent<HTMLDivElement>, item: DropdownItem) => void;
renderMenu?: (subMenu: JSX.Element, props: any) => JSX.Element;
renderMenuItem?: (item: DropdownItem, props: any) => JSX.Element;
}) => {
subItems.forEach((_, idx) => {
const newKey = `${parentKey}-${idx}`;
if (!subMenuRefs.current[newKey]) {
subMenuRefs.current[newKey] = React.createRef<HTMLDivElement>();
}
});
const position = subMenuPosition[parentKey];
const isPositioned = position && position.top !== undefined && position.left !== undefined;
const subMenu = (
<div
className="dropdown sub-dropdown"
ref={subMenuRefs.current[parentKey]}
style={{
top: position?.top || 0,
left: position?.left || 0,
position: "absolute",
zIndex: 1000,
visibility: visibleSubMenus[parentKey]?.visible && isPositioned ? "visible" : "hidden",
}}
>
{subItems.map((item, idx) => {
const newKey = `${parentKey}-${idx}`;
const isActive = hoveredItems.includes(newKey);
const menuItemProps = {
className: clsx("dropdown-item", { active: isActive }),
onMouseEnter: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) =>
handleMouseEnterItem(event, parentKey, idx, item),
onClick: (e: React.MouseEvent<HTMLDivElement>) => handleOnClick(e, item),
};
const renderedItem = renderMenuItem ? (
renderMenuItem(item, menuItemProps) // Remove portal here
) : (
<div key={newKey} {...menuItemProps}>
{item.label}
{item.subItems && <span className="arrow">â–¶</span>}
</div>
);
return (
<React.Fragment key={newKey}>
{renderedItem}
{visibleSubMenus[newKey]?.visible && item.subItems && (
<SubMenu
subItems={item.subItems}
parentKey={newKey}
subMenuPosition={subMenuPosition}
visibleSubMenus={visibleSubMenus}
hoveredItems={hoveredItems}
handleMouseEnterItem={handleMouseEnterItem}
handleOnClick={handleOnClick}
subMenuRefs={subMenuRefs}
renderMenu={renderMenu}
renderMenuItem={renderMenuItem}
/>
)}
</React.Fragment>
);
})}
</div>
);
return ReactDOM.createPortal(renderMenu ? renderMenu(subMenu, { parentKey }) : subMenu, document.body);
}
);
const Dropdown = memo(
({
items,
anchorRef,
boundaryRef,
className,
setVisibility,
renderMenu,
renderMenuItem,
}: {
items: DropdownItem[];
anchorRef: React.RefObject<HTMLElement>;
boundaryRef?: React.RefObject<HTMLElement>;
className?: string;
setVisibility: (_: boolean) => void;
renderMenu?: (subMenu: JSX.Element, props: any) => JSX.Element;
renderMenuItem?: (item: DropdownItem, props: any) => JSX.Element;
}) => {
const [visibleSubMenus, setVisibleSubMenus] = useState<{ [key: string]: any }>({});
const [hoveredItems, setHoveredItems] = useState<string[]>([]);
const [subMenuPosition, setSubMenuPosition] = useState<{
[key: string]: { top: number; left: number; label: string };
}>({});
const [position, setPosition] = useState({ top: 0, left: 0 });
const dropdownRef = useRef<HTMLDivElement>(null);
const subMenuRefs = useRef<{ [key: string]: React.RefObject<HTMLDivElement> }>({});
const effectiveBoundaryRef: React.RefObject<HTMLElement> = boundaryRef ?? { current: document.documentElement };
const width = useWidth(effectiveBoundaryRef);
const height = useHeight(effectiveBoundaryRef);
items.forEach((_, idx) => {
const key = `${idx}`;
if (!subMenuRefs.current[key]) {
subMenuRefs.current[key] = React.createRef<HTMLDivElement>();
}
});
useLayoutEffect(() => {
if (anchorRef.current && dropdownRef.current) {
const anchorRect = anchorRef.current.getBoundingClientRect();
const scrollTop = window.scrollY || document.documentElement.scrollTop;
const scrollLeft = window.scrollX || document.documentElement.scrollLeft;
let top = anchorRect.bottom + scrollTop;
let left = anchorRect.left + scrollLeft;
const boundaryRect = effectiveBoundaryRef.current?.getBoundingClientRect() || {
top: 0,
left: 0,
bottom: window.innerHeight,
right: window.innerWidth,
};
if (left + dropdownRef.current.offsetWidth > boundaryRect.right + scrollLeft) {
left = boundaryRect.right + scrollLeft - dropdownRef.current.offsetWidth;
}
if (top + dropdownRef.current.offsetHeight > boundaryRect.bottom + scrollTop) {
top = boundaryRect.bottom + scrollTop - dropdownRef.current.offsetHeight;
}
setPosition({ top, left });
}
}, [width, height]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const isClickInsideDropdown = dropdownRef.current && dropdownRef.current.contains(event.target as Node);
const isClickInsideAnchor = anchorRef.current && anchorRef.current.contains(event.target as Node);
const isClickInsideSubMenus = Object.keys(subMenuRefs.current).some(
(key) =>
subMenuRefs.current[key]?.current &&
subMenuRefs.current[key]?.current.contains(event.target as Node)
);
if (!isClickInsideDropdown && !isClickInsideAnchor && !isClickInsideSubMenus) {
setVisibility(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [dropdownRef, anchorRef, subMenuRefs]);
// Position submenus based on available space and scroll position
const handleSubMenuPosition = (
key: string,
itemRect: DOMRect,
parentRef: React.RefObject<HTMLDivElement>,
label: string
) => {
setTimeout(() => {
const subMenuRef = subMenuRefs.current[key]?.current;
if (!subMenuRef) return;
const scrollTop = window.scrollY || document.documentElement.scrollTop;
const scrollLeft = window.scrollX || document.documentElement.scrollLeft;
const boundaryRect = effectiveBoundaryRef.current?.getBoundingClientRect() || {
top: 0,
left: 0,
bottom: window.innerHeight,
right: window.innerWidth,
};
const submenuWidth = subMenuRef.offsetWidth;
const submenuHeight = subMenuRef.offsetHeight;
let left = itemRect.right + scrollLeft - 2; // Adjust for horizontal scroll
let top = itemRect.top + scrollTop; // Adjust for vertical scroll
// Adjust to the left if overflowing the right boundary
if (left + submenuWidth > window.innerWidth + scrollLeft) {
left = itemRect.left + scrollLeft - submenuWidth;
}
// Adjust if the submenu overflows the bottom boundary
if (top + submenuHeight > window.innerHeight + scrollTop) {
top = window.innerHeight + scrollTop - submenuHeight - 10;
}
setSubMenuPosition((prev) => ({
...prev,
[key]: { top, left, label },
}));
}, 0);
};
const handleMouseEnterItem = (
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
parentKey: string | null,
index: number,
item: DropdownItem
) => {
event.stopPropagation();
const key = parentKey ? `${parentKey}-${index}` : `${index}`;
setVisibleSubMenus((prev) => {
const updatedState = { ...prev };
updatedState[key] = { visible: true, label: item.label };
const ancestors = key.split("-").reduce((acc, part, idx) => {
if (idx === 0) return [part];
return [...acc, `${acc[idx - 1]}-${part}`];
}, [] as string[]);
ancestors.forEach((ancestorKey) => {
if (updatedState[ancestorKey]) {
updatedState[ancestorKey].visible = true;
}
});
for (const pkey in updatedState) {
if (!ancestors.includes(pkey) && pkey !== key) {
updatedState[pkey].visible = false;
}
}
return updatedState;
});
const newHoveredItems = key.split("-").reduce((acc, part, idx) => {
if (idx === 0) return [part];
return [...acc, `${acc[idx - 1]}-${part}`];
}, [] as string[]);
setHoveredItems(newHoveredItems);
const itemRect = event.currentTarget.getBoundingClientRect();
handleSubMenuPosition(key, itemRect, dropdownRef, item.label);
};
const handleOnClick = (e: React.MouseEvent<HTMLDivElement>, item: DropdownItem) => {
e.stopPropagation();
item.onClick && item.onClick(e);
};
const dropdownMenu = (
<div
className={clsx("dropdown", className)}
ref={dropdownRef}
style={{ top: position.top, left: position.left }}
>
{items.map((item, index) => {
const key = `${index}`;
const isActive = hoveredItems.includes(key);
const menuItemProps = {
className: clsx("dropdown-item", { active: isActive }),
onMouseEnter: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) =>
handleMouseEnterItem(event, null, index, item),
onClick: (e: React.MouseEvent<HTMLDivElement>) => handleOnClick(e, item),
};
const renderedItem = renderMenuItem ? (
renderMenuItem(item, menuItemProps) // No portal here
) : (
<div key={key} {...menuItemProps}>
{item.label}
{item.subItems && <span className="arrow">â–¶</span>}
</div>
);
return (
<React.Fragment key={key}>
{renderedItem}
{visibleSubMenus[key]?.visible && item.subItems && (
<SubMenu
subItems={item.subItems}
parentKey={key}
subMenuPosition={subMenuPosition}
visibleSubMenus={visibleSubMenus}
hoveredItems={hoveredItems}
handleMouseEnterItem={handleMouseEnterItem}
handleOnClick={handleOnClick}
subMenuRefs={subMenuRefs}
renderMenu={renderMenu}
renderMenuItem={renderMenuItem}
/>
)}
</React.Fragment>
);
})}
</div>
);
return ReactDOM.createPortal(
renderMenu ? renderMenu(dropdownMenu, { parentKey: null }) : dropdownMenu,
document.body
);
}
);
export { Dropdown };
+11
View File
@@ -144,6 +144,17 @@ const QuickTips = () => {
</div>
</div>
</div>
<div className="tip-section-header">Need More Help?</div>
<div className="tip">
<div>
<div>
<a target="_blank" href="https://discord.gg/XfvZ334gwU" rel="noopener">
Join Our Discord
</a>
</div>
</div>
</div>
</div>
</div>
);
+8 -3
View File
@@ -149,8 +149,11 @@ class CpuPlotViewModel {
if (initialData == null) {
return;
}
const newData = this.getDefaultData();
const initialDataItems: DataItem[] = initialData.map(convertWaveEventToDataItem);
globalStore.set(this.addDataAtom, initialDataItems);
// splice the initial data into the default data (replacing the newest points)
newData.splice(newData.length - initialDataItems.length, initialDataItems.length, ...initialDataItems);
globalStore.set(this.addDataAtom, newData);
} catch (e) {
console.log("Error loading initial data for cpuplot", e);
} finally {
@@ -158,7 +161,7 @@ class CpuPlotViewModel {
}
}
getDefaultData(): Array<DataItem> {
getDefaultData(): DataItem[] {
// set it back one to avoid backwards line being possible
const numPoints = globalStore.get(this.numPoints);
const currentTime = Date.now() - 1000;
@@ -197,6 +200,8 @@ function CpuPlotView({ model, blockId }: CpuPlotViewProps) {
lastConnName.current = connName;
model.loadInitialData();
}
}, [connStatus.status, connName]);
React.useEffect(() => {
const unsubFn = waveEventSubscribe({
eventType: "sysinfo",
scope: connName,
@@ -209,11 +214,11 @@ function CpuPlotView({ model, blockId }: CpuPlotViewProps) {
addPlotData([dataItem]);
},
});
console.log("subscribe to sysinfo", connName);
return () => {
unsubFn();
};
}, [connName]);
React.useEffect(() => {}, [connName]);
if (connStatus?.status != "connected") {
return null;
}
+24 -53
View File
@@ -97,54 +97,6 @@ function isStreamingType(mimeType: string): boolean {
mimeType.startsWith("image/")
);
}
const previewTipText = `
### Preview
Preview is the generic type of block used for viewing files. This can take many different forms based on the type of file being viewed.
You can use \`wsh view [path]\` from any Wave terminal window to open a preview block with the contents of the specified path (e.g. \`wsh view .\` or \`wsh view ~/myimage.jpg\`).
#### Directory
When looking at a directory, preview will show a file viewer much like MacOS' *Finder* application or Windows' *File Explorer* application. This variant is slightly more geared toward software development with the focus on seeing what is shown by the \`ls -alh\` command.
##### View a New File
The simplest way to view a new file is to double click its row in the file viewer. Alternatively, while the block is focused, you can use the &uarr; and &darr; arrow keys to select a row and press enter to preview the associated file.
##### View the Parent Directory
In the directory view, this is as simple as opening the \`..\` file as if it were a regular file. This can be done with the method above. You can also use the keyboard shortcut \`Cmd + ArrowUp\`.
##### Navigate Back and Forward
When looking at a file, you can navigate back by clicking the back button in the block header or the keyboard shortcut \`Cmd + ArrowLeft\`. You can always navigate back and forward using \`Cmd + ArrowLeft\` and \`Cmd + ArrowRight\`.
##### Filter the List of Files
While the block is focused, you can filter by filename by typing a substring of the filename you're working for. To clear the filter, you can click the &#x2715; on the filter dropdown or press esc.
##### Sort by a File Column
To sort a file by a specific column, click on the header for that column. If you click the header again, it will reverse the sort order.
##### Hide and Show Hidden Files
At the right of the block header, there is an &#128065;&#65039; button. Clicking this button hides and shows hidden files.
##### Refresh the Directory
At the right of the block header, there is a refresh button. Clicking this button refreshes the directory contents.
##### Navigate to Common Directories
At the left of the block header, there is a file icon. Clicking and holding on this icon opens a menu where you can select a common folder to navigate to. The available options are *Home*, *Desktop*, *Downloads*, and *Root*.
##### Open a New Terminal in the Current Directory
If you right click the header of the block (alternatively, click the gear icon), one of the menu items listed is **Open Terminal in New Block**. This will create a new terminal block at your current directory.
##### Open a New Terminal in a Child Directory
If you want to open a terminal for a child directory instead, you can right click on that file's row to get the **Open Terminal in New Block** option. Clicking this will open a terminal at that directory. Note that this option is only available for children that are directories.
##### Open a New Preview for a Child
To open a new Preview Block for a Child, you can right click on that file's row and select the **Open Preview in New Block** option.
#### Markdown
Opening a markdown file will bring up a view of the rendered markdown. These files cannot be edited in the preview at this time.
#### Images/Media
Opening a picture will bring up the image of that picture. Opening a video will bring up a player that lets you watch the video.
`;
export class PreviewModel implements ViewModel {
viewType: string;
blockId: string;
@@ -173,7 +125,8 @@ export class PreviewModel implements ViewModel {
fullFile: jotai.Atom<Promise<FullFile>>;
fileMimeType: jotai.Atom<Promise<string>>;
fileMimeTypeLoadable: jotai.Atom<Loadable<string>>;
fileContent: jotai.Atom<Promise<string>>;
fileContentSaved: jotai.PrimitiveAtom<string | null>;
fileContent: jotai.WritableAtom<Promise<string>, [string], void>;
newFileContent: jotai.PrimitiveAtom<string | null>;
openFileModal: jotai.PrimitiveAtom<boolean>;
@@ -191,6 +144,8 @@ export class PreviewModel implements ViewModel {
codeEditKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean;
setPreviewFileName(fileName: string) {
globalStore.set(this.fileContentSaved, null);
globalStore.set(this.newFileContent, null);
services.ObjectService.UpdateObjectMeta(`block:${this.blockId}`, { file: fileName });
}
@@ -430,10 +385,25 @@ export class PreviewModel implements ViewModel {
return file;
});
const fileContentAtom = jotai.atom<Promise<string>>(async (get) => {
const fullFile = await get(fullFileAtom);
return util.base64ToString(fullFile?.data64);
});
this.fileContentSaved = jotai.atom(null) as jotai.PrimitiveAtom<string | null>;
const fileContentAtom = jotai.atom(
async (get) => {
const _ = get(this.metaFilePath);
const newContent = get(this.newFileContent);
if (newContent != null) {
return newContent;
}
const savedContent = get(this.fileContentSaved);
if (savedContent != null) {
return savedContent;
}
const fullFile = await get(fullFileAtom);
return util.base64ToString(fullFile?.data64);
},
(get, set, update: string) => {
set(this.fileContentSaved, update);
}
);
this.fullFile = fullFileAtom;
this.fileContent = fileContentAtom;
@@ -590,6 +560,7 @@ export class PreviewModel implements ViewModel {
const conn = globalStore.get(this.connection) ?? "";
try {
services.FileService.SaveFile(conn, filePath, util.stringToBase64(newFileContent));
globalStore.set(this.fileContent, newFileContent);
globalStore.set(this.newFileContent, null);
console.log("saved file", filePath);
} catch (error) {
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
.quicktips-view {
padding: 10px 2px;
padding: 10px 5px;
overflow: auto;
width: 100%;
}
+52 -22
View File
@@ -1,8 +1,10 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { getApi, openLink } from "@/app/store/global";
import { getApi, openLink, useSettingsKeyAtom } from "@/app/store/global";
import { getSimpleControlShiftAtom } from "@/app/store/keymodel";
import { RpcApi } from "@/app/store/wshclientapi";
import { WindowRpcClient } from "@/app/store/wshrpcutil";
import { NodeModel } from "@/layout/index";
import { WOS, globalStore } from "@/store/global";
import * as services from "@/store/services";
@@ -30,6 +32,7 @@ export class WebViewModel implements ViewModel {
webviewRef: React.RefObject<WebviewTag>;
urlInputRef: React.RefObject<HTMLInputElement>;
nodeModel: NodeModel;
endIconButtons?: jotai.Atom<IconButtonDecl[]>;
constructor(blockId: string, nodeModel: NodeModel) {
this.nodeModel = nodeModel;
@@ -48,7 +51,8 @@ export class WebViewModel implements ViewModel {
this.webviewRef = React.createRef<WebviewTag>();
this.viewText = jotai.atom((get) => {
let url = get(this.blockAtom)?.meta?.url || "";
const defaultUrlAtom = useSettingsKeyAtom("web:defaulturl");
let url = get(this.blockAtom)?.meta?.url || get(defaultUrlAtom);
const currUrl = get(this.url);
if (currUrl !== undefined) {
url = currUrl;
@@ -91,6 +95,22 @@ export class WebViewModel implements ViewModel {
},
] as HeaderElem[];
});
this.endIconButtons = jotai.atom((get) => {
return [
{
elemtype: "iconbutton",
icon: "arrow-up-right-from-square",
title: "Open in External Browser",
click: () => {
const url = this.getUrl();
if (url != null && url != "") {
return getApi().openExternal(this.getUrl());
}
},
},
];
});
}
/**
@@ -199,7 +219,11 @@ export class WebViewModel implements ViewModel {
globalStore.set(this.url, url);
}
ensureUrlScheme(url: string) {
ensureUrlScheme(url: string, searchTemplate: string) {
if (url == null) {
url = "";
}
if (/^(http|https):/.test(url)) {
// If the URL starts with http: or https:, return it as is
return url;
@@ -223,23 +247,10 @@ export class WebViewModel implements ViewModel {
}
// Otherwise, treat it as a search query
return `https://www.google.com/search?q=${encodeURIComponent(url)}`;
}
normalizeUrl(url: string) {
if (!url) {
return url;
}
try {
const parsedUrl = new URL(url);
if (parsedUrl.hostname.startsWith("www.")) {
parsedUrl.hostname = parsedUrl.hostname.slice(4);
}
return parsedUrl.href;
} catch (e) {
return url.replace(/\/+$/, "") + "/";
if (searchTemplate == null) {
return `https://www.google.com/search?q=${encodeURIComponent(url)}`;
}
return searchTemplate.replace("{query}", encodeURIComponent(url));
}
/**
@@ -247,7 +258,9 @@ export class WebViewModel implements ViewModel {
* @param newUrl The new URL to load in the webview.
*/
loadUrl(newUrl: string, reason: string) {
const nextUrl = this.ensureUrlScheme(newUrl);
const defaultSearchAtom = useSettingsKeyAtom("web:defaultsearch");
const searchTemplate = globalStore.get(defaultSearchAtom);
const nextUrl = this.ensureUrlScheme(newUrl, searchTemplate);
console.log("webview loadUrl", reason, nextUrl, "cur=", this.webviewRef?.current.getURL());
if (newUrl != nextUrl) {
globalStore.set(this.url, nextUrl);
@@ -317,8 +330,20 @@ export class WebViewModel implements ViewModel {
return false;
}
getSettingsMenuItems() {
getSettingsMenuItems(): ContextMenuItem[] {
return [
{
label: "Set Homepage",
click: async () => {
const url = this.getUrl();
if (url != null && url != "") {
RpcApi.SetConfigCommand(WindowRpcClient, { "web:defaulturl": url });
}
},
},
{
type: "separator",
},
{
label: this.webviewRef.current?.isDevToolsOpened() ? "Close DevTools" : "Open DevTools",
click: async () => {
@@ -347,7 +372,12 @@ interface WebViewProps {
const WebView = memo(({ model }: WebViewProps) => {
const blockData = jotai.useAtomValue(model.blockAtom);
const metaUrl = blockData?.meta?.url;
const defaultUrlAtom = useSettingsKeyAtom("web:defaulturl");
const defaultUrl = jotai.useAtomValue(defaultUrlAtom);
const defaultSearchAtom = useSettingsKeyAtom("web:defaultsearch");
const defaultSearch = jotai.useAtomValue(defaultSearchAtom);
let metaUrl = blockData?.meta?.url || defaultUrl;
metaUrl = model.ensureUrlScheme(metaUrl, defaultSearch);
const metaUrlRef = React.useRef(metaUrl);
// The initial value of the block metadata URL when the component first renders. Used to set the starting src value for the webview.
+2
View File
@@ -421,6 +421,8 @@ declare global {
"editor:stickyscrollenabled"?: boolean;
"web:*"?: boolean;
"web:openlinksinternally"?: boolean;
"web:defaulturl"?: string;
"web:defaultsearch"?: string;
"blockheader:*"?: boolean;
"blockheader:showblockids"?: boolean;
"autoupdate:*"?: boolean;
+9 -9
View File
@@ -7,7 +7,7 @@
"productName": "Wave",
"description": "Open-Source AI-Native Terminal Built for Seamless Workflows",
"license": "Apache-2.0",
"version": "0.8.0-beta.7",
"version": "0.8.0-beta.14",
"homepage": "https://waveterm.dev",
"build": {
"appId": "dev.commandline.waveterm"
@@ -30,13 +30,13 @@
"@chromatic-com/storybook": "^2.0.2",
"@eslint/js": "^9.10.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@storybook/addon-essentials": "^8.3.2",
"@storybook/addon-interactions": "^8.3.2",
"@storybook/addon-links": "^8.3.2",
"@storybook/blocks": "^8.3.2",
"@storybook/react": "^8.3.2",
"@storybook/react-vite": "^8.3.2",
"@storybook/test": "^8.3.2",
"@storybook/addon-essentials": "^8.3.0",
"@storybook/addon-interactions": "^8.3.0",
"@storybook/addon-links": "^8.3.0",
"@storybook/blocks": "^8.3.0",
"@storybook/react": "^8.3.0",
"@storybook/react-vite": "^8.3.0",
"@storybook/test": "^8.3.0",
"@types/css-tree": "^2",
"@types/debug": "^4",
"@types/electron": "^1.6.10",
@@ -65,7 +65,7 @@
"prettier-plugin-organize-imports": "^4.0.0",
"rollup-plugin-flow": "^1.1.1",
"semver": "^7.6.3",
"storybook": "^8.3.2",
"storybook": "^8.3.0",
"ts-node": "^10.9.2",
"tslib": "^2.6.3",
"tsx": "^4.19.1",
+2 -2
View File
@@ -166,7 +166,7 @@ func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts Comm
} else if remote.IsPowershell(shellPath) {
// powershell is weird about quoted path executables and requires an ampersand first
shellPath = "& " + shellPath
shellOpts = append(shellOpts, "-NoExit", "-File", homeDir+fmt.Sprintf("/.waveterm/%s/wavepwsh.ps1", shellutil.PwshIntegrationDir))
shellOpts = append(shellOpts, "-ExecutionPolicy", "Bypass", "-NoExit", "-File", homeDir+fmt.Sprintf("/.waveterm/%s/wavepwsh.ps1", shellutil.PwshIntegrationDir))
} else {
if cmdOpts.Login {
shellOpts = append(shellOpts, "-l")
@@ -276,7 +276,7 @@ func StartShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOpt
// cant set -l or -i with --rcfile
shellOpts = append(shellOpts, "--rcfile", shellutil.GetBashRcFileOverride())
} else if remote.IsPowershell(shellPath) {
shellOpts = append(shellOpts, "-NoExit", "-File", shellutil.GetWavePowershellEnv())
shellOpts = append(shellOpts, "-ExecutionPolicy", "Bypass", "-NoExit", "-File", shellutil.GetWavePowershellEnv())
} else {
if cmdOpts.Login {
shellOpts = append(shellOpts, "-l")
+4 -1
View File
@@ -629,7 +629,7 @@ func CopyToChannel(outputCh chan<- []byte, reader io.Reader) error {
// on error just returns ""
// does not return "application/octet-stream" as this is considered a detection failure
// can pass an existing fileInfo to avoid re-statting the file
func DetectMimeType(path string, fileInfo fs.FileInfo) string {
func DetectMimeType(path string, fileInfo fs.FileInfo, extended bool) string {
if fileInfo == nil {
statRtn, err := os.Stat(path)
if err != nil {
@@ -657,6 +657,9 @@ func DetectMimeType(path string, fileInfo fs.FileInfo) string {
if mimeType := mime.TypeByExtension(ext); mimeType != "" {
return mimeType
}
if !extended {
return ""
}
fd, err := os.Open(path)
if err != nil {
return ""
@@ -27,8 +27,7 @@
"label": "web",
"blockdef": {
"meta": {
"view": "web",
"url": "https://waveterm.dev/"
"view": "web"
}
}
},
+2
View File
@@ -6,6 +6,8 @@
"autoupdate:installonquit": true,
"autoupdate:intervalms": 3600000,
"editor:minimapenabled": true,
"web:defaulturl": "https://github.com/wavetermdev/waveterm",
"web:defaultsearch": "https://www.google.com/search?q={query}",
"window:tilegapsize": 3,
"telemetry:enabled": true
}

Some files were not shown because too many files have changed in this diff Show More