mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-22 16:48:23 +01:00
Merge branch 'main' into red/tabs-new-design-2
This commit is contained in:
commit
1b8ac7f8bf
@ -274,6 +274,7 @@ export class WaveBrowserWindow extends BaseWindow {
|
|||||||
console.log("switchWorkspace newWs", newWs);
|
console.log("switchWorkspace newWs", newWs);
|
||||||
if (this.allTabViews.size) {
|
if (this.allTabViews.size) {
|
||||||
for (const tab of this.allTabViews.values()) {
|
for (const tab of this.allTabViews.values()) {
|
||||||
|
this.contentView.removeChildView(tab);
|
||||||
tab?.destroy();
|
tab?.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,7 @@ import {
|
|||||||
getAllWaveWindows,
|
getAllWaveWindows,
|
||||||
getWaveWindowById,
|
getWaveWindowById,
|
||||||
getWaveWindowByWebContentsId,
|
getWaveWindowByWebContentsId,
|
||||||
|
getWaveWindowByWorkspaceId,
|
||||||
WaveBrowserWindow,
|
WaveBrowserWindow,
|
||||||
} from "./emain-window";
|
} from "./emain-window";
|
||||||
import { ElectronWshClient, initElectronWshClient } from "./emain-wsh";
|
import { ElectronWshClient, initElectronWshClient } from "./emain-wsh";
|
||||||
@ -125,6 +126,14 @@ function handleWSEvent(evtMsg: WSEventType) {
|
|||||||
if (ww != null) {
|
if (ww != null) {
|
||||||
ww.destroy(); // bypass the "are you sure?" dialog
|
ww.destroy(); // bypass the "are you sure?" dialog
|
||||||
}
|
}
|
||||||
|
} else if (evtMsg.eventtype == "electron:updateactivetab") {
|
||||||
|
const activeTabUpdate: { workspaceid: string; newactivetabid: string } = evtMsg.data;
|
||||||
|
console.log("electron:updateactivetab", activeTabUpdate);
|
||||||
|
const ww = getWaveWindowByWorkspaceId(activeTabUpdate.workspaceid);
|
||||||
|
if (ww == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await ww.setActiveTab(activeTabUpdate.newactivetabid, false);
|
||||||
} else {
|
} else {
|
||||||
console.log("unhandled electron ws eventtype", evtMsg.eventtype);
|
console.log("unhandled electron ws eventtype", evtMsg.eventtype);
|
||||||
}
|
}
|
||||||
@ -359,9 +368,11 @@ electron.ipcMain.on("quicklook", (event, filePath: string) => {
|
|||||||
|
|
||||||
electron.ipcMain.on("open-native-path", (event, filePath: string) => {
|
electron.ipcMain.on("open-native-path", (event, filePath: string) => {
|
||||||
console.log("open-native-path", filePath);
|
console.log("open-native-path", filePath);
|
||||||
electron.shell.openPath(filePath).catch((err) => {
|
fireAndForget(async () =>
|
||||||
console.error(`Failed to open path ${filePath}:`, err);
|
electron.shell.openPath(filePath).then((excuse) => {
|
||||||
});
|
if (excuse) console.error(`Failed to open ${filePath} in native application: ${excuse}`);
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function createNewWaveWindow(): Promise<void> {
|
async function createNewWaveWindow(): Promise<void> {
|
||||||
|
@ -2,15 +2,15 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
.popover-content {
|
.popover-content {
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
min-height: 150px;
|
min-height: 150px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1000; // TODO: put this in theme.scss
|
z-index: 1000; // TODO: put this in theme.scss
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
gap: 1px;
|
gap: 1px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
background: #212121;
|
background: #212121;
|
||||||
box-shadow: 0px 8px 24px 0px rgba(0, 0, 0, 0.3);
|
box-shadow: 0px 8px 24px 0px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import { Button } from "@/element/button";
|
|||||||
import {
|
import {
|
||||||
autoUpdate,
|
autoUpdate,
|
||||||
FloatingPortal,
|
FloatingPortal,
|
||||||
|
Middleware,
|
||||||
offset as offsetMiddleware,
|
offset as offsetMiddleware,
|
||||||
useClick,
|
useClick,
|
||||||
useDismiss,
|
useDismiss,
|
||||||
@ -34,6 +35,7 @@ interface PopoverProps {
|
|||||||
placement?: Placement;
|
placement?: Placement;
|
||||||
offset?: OffsetOptions;
|
offset?: OffsetOptions;
|
||||||
onDismiss?: () => void;
|
onDismiss?: () => void;
|
||||||
|
middleware?: Middleware[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPopoverButton = (
|
const isPopoverButton = (
|
||||||
|
@ -557,6 +557,7 @@ function getConnStatusAtom(conn: string): PrimitiveAtom<ConnStatus> {
|
|||||||
status: "connected",
|
status: "connected",
|
||||||
hasconnected: true,
|
hasconnected: true,
|
||||||
activeconnnum: 0,
|
activeconnnum: 0,
|
||||||
|
wshenabled: false,
|
||||||
};
|
};
|
||||||
rtn = atom(connStatus);
|
rtn = atom(connStatus);
|
||||||
} else {
|
} else {
|
||||||
@ -567,6 +568,7 @@ function getConnStatusAtom(conn: string): PrimitiveAtom<ConnStatus> {
|
|||||||
status: "disconnected",
|
status: "disconnected",
|
||||||
hasconnected: false,
|
hasconnected: false,
|
||||||
activeconnnum: 0,
|
activeconnnum: 0,
|
||||||
|
wshenabled: false,
|
||||||
};
|
};
|
||||||
rtn = atom(connStatus);
|
rtn = atom(connStatus);
|
||||||
}
|
}
|
||||||
@ -620,8 +622,13 @@ function removeNotification(id: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createTab(): Promise<void> {
|
function createTab() {
|
||||||
await getApi().createTab();
|
getApi().createTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActiveTab(tabId: string) {
|
||||||
|
document.body.classList.add("nohover");
|
||||||
|
getApi().setActiveTab(tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -655,6 +662,7 @@ export {
|
|||||||
removeFlashError,
|
removeFlashError,
|
||||||
removeNotification,
|
removeNotification,
|
||||||
removeNotificationById,
|
removeNotificationById,
|
||||||
|
setActiveTab,
|
||||||
setNodeFocus,
|
setNodeFocus,
|
||||||
setPlatform,
|
setPlatform,
|
||||||
subscribeToConnEvents,
|
subscribeToConnEvents,
|
||||||
|
@ -59,11 +59,17 @@ class FileServiceType {
|
|||||||
GetWaveFile(arg1: string, arg2: string): Promise<any> {
|
GetWaveFile(arg1: string, arg2: string): Promise<any> {
|
||||||
return WOS.callBackendService("file", "GetWaveFile", Array.from(arguments))
|
return WOS.callBackendService("file", "GetWaveFile", Array.from(arguments))
|
||||||
}
|
}
|
||||||
|
Mkdir(arg1: string, arg2: string): Promise<void> {
|
||||||
|
return WOS.callBackendService("file", "Mkdir", Array.from(arguments))
|
||||||
|
}
|
||||||
|
|
||||||
// read file
|
// read file
|
||||||
ReadFile(connection: string, path: string): Promise<FullFile> {
|
ReadFile(connection: string, path: string): Promise<FullFile> {
|
||||||
return WOS.callBackendService("file", "ReadFile", Array.from(arguments))
|
return WOS.callBackendService("file", "ReadFile", Array.from(arguments))
|
||||||
}
|
}
|
||||||
|
Rename(arg1: string, arg2: string, arg3: string): Promise<void> {
|
||||||
|
return WOS.callBackendService("file", "Rename", Array.from(arguments))
|
||||||
|
}
|
||||||
|
|
||||||
// save file
|
// save file
|
||||||
SaveFile(connection: string, path: string, data64: string): Promise<void> {
|
SaveFile(connection: string, path: string, data64: string): Promise<void> {
|
||||||
@ -74,6 +80,9 @@ class FileServiceType {
|
|||||||
StatFile(connection: string, path: string): Promise<FileInfo> {
|
StatFile(connection: string, path: string): Promise<FileInfo> {
|
||||||
return WOS.callBackendService("file", "StatFile", Array.from(arguments))
|
return WOS.callBackendService("file", "StatFile", Array.from(arguments))
|
||||||
}
|
}
|
||||||
|
TouchFile(arg1: string, arg2: string): Promise<void> {
|
||||||
|
return WOS.callBackendService("file", "TouchFile", Array.from(arguments))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileService = new FileServiceType();
|
export const FileService = new FileServiceType();
|
||||||
|
@ -212,6 +212,21 @@ class RpcApiType {
|
|||||||
return client.wshRpcCall("remotefilejoin", data, opts);
|
return client.wshRpcCall("remotefilejoin", data, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// command "remotefilerename" [call]
|
||||||
|
RemoteFileRenameCommand(client: WshClient, data: string[], opts?: RpcOpts): Promise<void> {
|
||||||
|
return client.wshRpcCall("remotefilerename", data, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// command "remotefiletouch" [call]
|
||||||
|
RemoteFileTouchCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> {
|
||||||
|
return client.wshRpcCall("remotefiletouch", data, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// command "remotemkdir" [call]
|
||||||
|
RemoteMkdirCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> {
|
||||||
|
return client.wshRpcCall("remotemkdir", data, opts);
|
||||||
|
}
|
||||||
|
|
||||||
// command "remotestreamcpudata" [responsestream]
|
// command "remotestreamcpudata" [responsestream]
|
||||||
RemoteStreamCpuDataCommand(client: WshClient, opts?: RpcOpts): AsyncGenerator<TimeSeriesData, void, boolean> {
|
RemoteStreamCpuDataCommand(client: WshClient, opts?: RpcOpts): AsyncGenerator<TimeSeriesData, void, boolean> {
|
||||||
return client.wshRpcStream("remotestreamcpudata", null, opts);
|
return client.wshRpcStream("remotestreamcpudata", null, opts);
|
||||||
|
@ -33,13 +33,6 @@
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.tab-inner {
|
|
||||||
border-color: transparent;
|
|
||||||
background: rgb(from var(--main-text-color) r g b / 0.07);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.animate {
|
&.animate {
|
||||||
transition:
|
transition:
|
||||||
transform 0.3s ease,
|
transform 0.3s ease,
|
||||||
@ -87,19 +80,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
transform: translate3d(0, -50%, 0);
|
transform: translate3d(0, -50%, 0);
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: var(--zindex-tab-name);
|
z-index: var(--zindex-tab-name);
|
||||||
padding: 1px 2px;
|
padding: 1px 2px;
|
||||||
|
transition: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover .close {
|
&:hover .close {
|
||||||
@ -112,6 +106,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body:not(.nohover) .tab:hover {
|
||||||
|
.tab-inner {
|
||||||
|
border-color: transparent;
|
||||||
|
background: rgb(from var(--main-text-color) r g b / 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
visibility: visible;
|
||||||
|
&:hover {
|
||||||
|
color: var(--main-text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes expandWidthAndFadeIn {
|
@keyframes expandWidthAndFadeIn {
|
||||||
from {
|
from {
|
||||||
width: var(--initial-tab-width);
|
width: var(--initial-tab-width);
|
||||||
|
@ -5,7 +5,7 @@ import { Button } from "@/app/element/button";
|
|||||||
import { modalsModel } from "@/app/store/modalmodel";
|
import { modalsModel } from "@/app/store/modalmodel";
|
||||||
import { WindowDrag } from "@/element/windowdrag";
|
import { WindowDrag } from "@/element/windowdrag";
|
||||||
import { deleteLayoutModelForTab } from "@/layout/index";
|
import { deleteLayoutModelForTab } from "@/layout/index";
|
||||||
import { atoms, createTab, getApi, isDev, PLATFORM } from "@/store/global";
|
import { atoms, createTab, getApi, isDev, PLATFORM, setActiveTab } from "@/store/global";
|
||||||
import { fireAndForget } from "@/util/util";
|
import { fireAndForget } from "@/util/util";
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
import { OverlayScrollbars } from "overlayscrollbars";
|
import { OverlayScrollbars } from "overlayscrollbars";
|
||||||
@ -500,7 +500,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
|
|
||||||
const handleSelectTab = (tabId: string) => {
|
const handleSelectTab = (tabId: string) => {
|
||||||
if (!draggingTabDataRef.current.dragged) {
|
if (!draggingTabDataRef.current.dragged) {
|
||||||
getApi().setActiveTab(tabId);
|
setActiveTab(tabId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -207,7 +207,7 @@ const WorkspaceSwitcher = forwardRef<HTMLDivElement, {}>(({}, ref) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover ref={ref} className="workspace-switcher-popover" onDismiss={() => setEditingWorkspace(null)}>
|
<Popover ref={ref} className="workspace-switcher-popover" placement="bottom-start" onDismiss={() => setEditingWorkspace(null)}>
|
||||||
<PopoverButton
|
<PopoverButton
|
||||||
className="workspace-switcher-button grey"
|
className="workspace-switcher-button grey"
|
||||||
as="div"
|
as="div"
|
||||||
|
@ -230,3 +230,17 @@
|
|||||||
background-color: var(--highlight-bg-color);
|
background-color: var(--highlight-bg-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entry-manager-overlay {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: fit-content;
|
||||||
|
display: flex;
|
||||||
|
padding: 10px;
|
||||||
|
gap: 10px;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import { Input } from "@/app/element/input";
|
||||||
import { ContextMenuModel } from "@/app/store/contextmenu";
|
import { ContextMenuModel } from "@/app/store/contextmenu";
|
||||||
import { PLATFORM, atoms, createBlock, getApi } from "@/app/store/global";
|
import { PLATFORM, atoms, createBlock, getApi, globalStore } from "@/app/store/global";
|
||||||
import { FileService } from "@/app/store/services";
|
import { FileService } from "@/app/store/services";
|
||||||
import type { PreviewModel } from "@/app/view/preview/preview";
|
import type { PreviewModel } from "@/app/view/preview/preview";
|
||||||
import { checkKeyPressed, isCharacterKeyEvent } from "@/util/keyutil";
|
import { checkKeyPressed, isCharacterKeyEvent } from "@/util/keyutil";
|
||||||
import { base64ToString, isBlank } from "@/util/util";
|
import { base64ToString, fireAndForget, isBlank } from "@/util/util";
|
||||||
|
import { offset, useDismiss, useFloating, useInteractions } from "@floating-ui/react";
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
Row,
|
Row,
|
||||||
|
RowData,
|
||||||
Table,
|
Table,
|
||||||
createColumnHelper,
|
createColumnHelper,
|
||||||
flexRender,
|
flexRender,
|
||||||
@ -19,13 +22,21 @@ import {
|
|||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useAtom, useAtomValue } from "jotai";
|
import { PrimitiveAtom, atom, useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||||
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
|
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import React, { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { quote as shellQuote } from "shell-quote";
|
import { quote as shellQuote } from "shell-quote";
|
||||||
import { debounce } from "throttle-debounce";
|
import { debounce } from "throttle-debounce";
|
||||||
import "./directorypreview.scss";
|
import "./directorypreview.scss";
|
||||||
|
|
||||||
|
declare module "@tanstack/react-table" {
|
||||||
|
interface TableMeta<TData extends RowData> {
|
||||||
|
updateName: (path: string) => void;
|
||||||
|
newFile: () => void;
|
||||||
|
newDirectory: () => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface DirectoryTableProps {
|
interface DirectoryTableProps {
|
||||||
model: PreviewModel;
|
model: PreviewModel;
|
||||||
data: FileInfo[];
|
data: FileInfo[];
|
||||||
@ -35,6 +46,9 @@ interface DirectoryTableProps {
|
|||||||
setSearch: (_: string) => void;
|
setSearch: (_: string) => void;
|
||||||
setSelectedPath: (_: string) => void;
|
setSelectedPath: (_: string) => void;
|
||||||
setRefreshVersion: React.Dispatch<React.SetStateAction<number>>;
|
setRefreshVersion: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
entryManagerOverlayPropsAtom: PrimitiveAtom<EntryManagerOverlayProps>;
|
||||||
|
newFile: () => void;
|
||||||
|
newDirectory: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<FileInfo>();
|
const columnHelper = createColumnHelper<FileInfo>();
|
||||||
@ -121,6 +135,46 @@ function cleanMimetype(input: string): string {
|
|||||||
return truncated.trim();
|
return truncated.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum EntryManagerType {
|
||||||
|
NewFile = "New File",
|
||||||
|
NewDirectory = "New Folder",
|
||||||
|
EditName = "Rename",
|
||||||
|
}
|
||||||
|
|
||||||
|
type EntryManagerOverlayProps = {
|
||||||
|
forwardRef?: React.Ref<HTMLDivElement>;
|
||||||
|
entryManagerType: EntryManagerType;
|
||||||
|
startingValue?: string;
|
||||||
|
onSave: (newValue: string) => void;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
getReferenceProps?: () => any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EntryManagerOverlay = memo(
|
||||||
|
({ entryManagerType, startingValue, onSave, forwardRef, style, getReferenceProps }: EntryManagerOverlayProps) => {
|
||||||
|
const [value, setValue] = useState(startingValue);
|
||||||
|
return (
|
||||||
|
<div className="entry-manager-overlay" ref={forwardRef} style={style} {...getReferenceProps()}>
|
||||||
|
<div className="entry-manager-type">{entryManagerType}</div>
|
||||||
|
<div className="entry-manager-input">
|
||||||
|
<Input
|
||||||
|
value={value}
|
||||||
|
onChange={setValue}
|
||||||
|
autoFocus={true}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onSave(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></Input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function DirectoryTable({
|
function DirectoryTable({
|
||||||
model,
|
model,
|
||||||
data,
|
data,
|
||||||
@ -130,6 +184,9 @@ function DirectoryTable({
|
|||||||
setSearch,
|
setSearch,
|
||||||
setSelectedPath,
|
setSelectedPath,
|
||||||
setRefreshVersion,
|
setRefreshVersion,
|
||||||
|
entryManagerOverlayPropsAtom,
|
||||||
|
newFile,
|
||||||
|
newDirectory,
|
||||||
}: DirectoryTableProps) {
|
}: DirectoryTableProps) {
|
||||||
const fullConfig = useAtomValue(atoms.fullConfigAtom);
|
const fullConfig = useAtomValue(atoms.fullConfigAtom);
|
||||||
const getIconFromMimeType = useCallback(
|
const getIconFromMimeType = useCallback(
|
||||||
@ -205,6 +262,28 @@ function DirectoryTable({
|
|||||||
[fullConfig]
|
[fullConfig]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setEntryManagerProps = useSetAtom(entryManagerOverlayPropsAtom);
|
||||||
|
|
||||||
|
const updateName = useCallback((path: string) => {
|
||||||
|
const fileName = path.split("/").at(-1);
|
||||||
|
setEntryManagerProps({
|
||||||
|
entryManagerType: EntryManagerType.EditName,
|
||||||
|
startingValue: fileName,
|
||||||
|
onSave: (newName: string) => {
|
||||||
|
let newPath: string;
|
||||||
|
if (newName !== fileName) {
|
||||||
|
newPath = path.replace(fileName, newName);
|
||||||
|
console.log(`replacing ${fileName} with ${newName}: ${path}`);
|
||||||
|
fireAndForget(async () => {
|
||||||
|
await FileService.Rename(globalStore.get(model.connection), path, newPath);
|
||||||
|
model.refreshCallback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setEntryManagerProps(undefined);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
@ -229,6 +308,11 @@ function DirectoryTable({
|
|||||||
},
|
},
|
||||||
enableMultiSort: false,
|
enableMultiSort: false,
|
||||||
enableSortingRemoval: false,
|
enableSortingRemoval: false,
|
||||||
|
meta: {
|
||||||
|
updateName,
|
||||||
|
newFile,
|
||||||
|
newDirectory,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -418,6 +502,27 @@ function TableBody({
|
|||||||
openNativeLabel = "Open File in Default Application";
|
openNativeLabel = "Open File in Default Application";
|
||||||
}
|
}
|
||||||
const menu: ContextMenuItem[] = [
|
const menu: ContextMenuItem[] = [
|
||||||
|
{
|
||||||
|
label: "New File",
|
||||||
|
click: () => {
|
||||||
|
table.options.meta.newFile();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "New Folder",
|
||||||
|
click: () => {
|
||||||
|
table.options.meta.newDirectory();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Rename",
|
||||||
|
click: () => {
|
||||||
|
table.options.meta.updateName(finfo.path);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "separator",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Copy File Name",
|
label: "Copy File Name",
|
||||||
click: () => navigator.clipboard.writeText(fileName),
|
click: () => navigator.clipboard.writeText(fileName),
|
||||||
@ -446,6 +551,7 @@ function TableBody({
|
|||||||
{
|
{
|
||||||
type: "separator",
|
type: "separator",
|
||||||
},
|
},
|
||||||
|
// TODO: Only show this option for local files, resolve correct host path if connection is WSL
|
||||||
{
|
{
|
||||||
label: openNativeLabel,
|
label: openNativeLabel,
|
||||||
click: async () => {
|
click: async () => {
|
||||||
@ -483,14 +589,18 @@ function TableBody({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
menu.push({ type: "separator" });
|
menu.push(
|
||||||
menu.push({
|
{
|
||||||
label: "Delete File",
|
type: "separator",
|
||||||
click: async () => {
|
|
||||||
await FileService.DeleteFile(conn, finfo.path).catch((e) => console.log(e));
|
|
||||||
setRefreshVersion((current) => current + 1);
|
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
|
label: "Delete",
|
||||||
|
click: async () => {
|
||||||
|
await FileService.DeleteFile(conn, finfo.path).catch((e) => console.log(e));
|
||||||
|
setRefreshVersion((current) => current + 1);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
ContextMenuModel.showContextMenu(menu, e);
|
ContextMenuModel.showContextMenu(menu, e);
|
||||||
},
|
},
|
||||||
[setRefreshVersion, conn]
|
[setRefreshVersion, conn]
|
||||||
@ -560,12 +670,12 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
|
|||||||
const [focusIndex, setFocusIndex] = useState(0);
|
const [focusIndex, setFocusIndex] = useState(0);
|
||||||
const [unfilteredData, setUnfilteredData] = useState<FileInfo[]>([]);
|
const [unfilteredData, setUnfilteredData] = useState<FileInfo[]>([]);
|
||||||
const [filteredData, setFilteredData] = useState<FileInfo[]>([]);
|
const [filteredData, setFilteredData] = useState<FileInfo[]>([]);
|
||||||
const fileName = useAtomValue(model.metaFilePath);
|
|
||||||
const showHiddenFiles = useAtomValue(model.showHiddenFiles);
|
const showHiddenFiles = useAtomValue(model.showHiddenFiles);
|
||||||
const [selectedPath, setSelectedPath] = useState("");
|
const [selectedPath, setSelectedPath] = useState("");
|
||||||
const [refreshVersion, setRefreshVersion] = useAtom(model.refreshVersion);
|
const [refreshVersion, setRefreshVersion] = useAtom(model.refreshVersion);
|
||||||
const conn = useAtomValue(model.connection);
|
const conn = useAtomValue(model.connection);
|
||||||
const blockData = useAtomValue(model.blockAtom);
|
const blockData = useAtomValue(model.blockAtom);
|
||||||
|
const dirPath = useAtomValue(model.normFilePath);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
model.refreshCallback = () => {
|
model.refreshCallback = () => {
|
||||||
@ -578,13 +688,13 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getContent = async () => {
|
const getContent = async () => {
|
||||||
const file = await FileService.ReadFile(conn, fileName);
|
const file = await FileService.ReadFile(conn, dirPath);
|
||||||
const serializedContent = base64ToString(file?.data64);
|
const serializedContent = base64ToString(file?.data64);
|
||||||
const content: FileInfo[] = JSON.parse(serializedContent);
|
const content: FileInfo[] = JSON.parse(serializedContent);
|
||||||
setUnfilteredData(content);
|
setUnfilteredData(content);
|
||||||
};
|
};
|
||||||
getContent();
|
getContent();
|
||||||
}, [conn, fileName, refreshVersion]);
|
}, [conn, dirPath, refreshVersion]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filtered = unfilteredData.filter((fileInfo) => {
|
const filtered = unfilteredData.filter((fileInfo) => {
|
||||||
@ -652,26 +762,138 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
|
|||||||
}
|
}
|
||||||
}, [filteredData]);
|
}, [filteredData]);
|
||||||
|
|
||||||
|
const entryManagerPropsAtom = useState(
|
||||||
|
atom<EntryManagerOverlayProps>(null) as PrimitiveAtom<EntryManagerOverlayProps>
|
||||||
|
)[0];
|
||||||
|
const [entryManagerProps, setEntryManagerProps] = useAtom(entryManagerPropsAtom);
|
||||||
|
|
||||||
|
const { refs, floatingStyles, context } = useFloating({
|
||||||
|
open: !!entryManagerProps,
|
||||||
|
onOpenChange: () => setEntryManagerProps(undefined),
|
||||||
|
middleware: [offset(({ rects }) => -rects.reference.height / 2 - rects.floating.height / 2)],
|
||||||
|
});
|
||||||
|
|
||||||
|
const dismiss = useDismiss(context);
|
||||||
|
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss]);
|
||||||
|
|
||||||
|
const newFile = useCallback(() => {
|
||||||
|
setEntryManagerProps({
|
||||||
|
entryManagerType: EntryManagerType.NewFile,
|
||||||
|
onSave: (newName: string) => {
|
||||||
|
console.log(`newFile: ${newName}`);
|
||||||
|
fireAndForget(async () => {
|
||||||
|
await FileService.TouchFile(globalStore.get(model.connection), `${dirPath}/${newName}`);
|
||||||
|
model.refreshCallback();
|
||||||
|
});
|
||||||
|
setEntryManagerProps(undefined);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [dirPath]);
|
||||||
|
const newDirectory = useCallback(() => {
|
||||||
|
setEntryManagerProps({
|
||||||
|
entryManagerType: EntryManagerType.NewDirectory,
|
||||||
|
onSave: (newName: string) => {
|
||||||
|
console.log(`newDirectory: ${newName}`);
|
||||||
|
fireAndForget(async () => {
|
||||||
|
await FileService.Mkdir(globalStore.get(model.connection), `${dirPath}/${newName}`);
|
||||||
|
model.refreshCallback();
|
||||||
|
});
|
||||||
|
setEntryManagerProps(undefined);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [dirPath]);
|
||||||
|
|
||||||
|
const handleFileContextMenu = useCallback(
|
||||||
|
(e: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
let openNativeLabel = "Open Directory in File Manager";
|
||||||
|
if (PLATFORM == "darwin") {
|
||||||
|
openNativeLabel = "Open Directory in Finder";
|
||||||
|
} else if (PLATFORM == "win32") {
|
||||||
|
openNativeLabel = "Open Directory in Explorer";
|
||||||
|
}
|
||||||
|
const menu: ContextMenuItem[] = [
|
||||||
|
{
|
||||||
|
label: "New File",
|
||||||
|
click: () => {
|
||||||
|
newFile();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "New Folder",
|
||||||
|
click: () => {
|
||||||
|
newDirectory();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "separator",
|
||||||
|
},
|
||||||
|
// TODO: Only show this option for local files, resolve correct host path if connection is WSL
|
||||||
|
{
|
||||||
|
label: openNativeLabel,
|
||||||
|
click: () => {
|
||||||
|
console.log(`opening ${dirPath}`);
|
||||||
|
getApi().openNativePath(dirPath);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
menu.push({
|
||||||
|
label: "Open Terminal in New Block",
|
||||||
|
click: async () => {
|
||||||
|
const termBlockDef: BlockDef = {
|
||||||
|
meta: {
|
||||||
|
controller: "shell",
|
||||||
|
view: "term",
|
||||||
|
"cmd:cwd": dirPath,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await createBlock(termBlockDef);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
ContextMenuModel.showContextMenu(menu, e);
|
||||||
|
},
|
||||||
|
[setRefreshVersion, conn, newFile, newDirectory, dirPath]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Fragment>
|
||||||
className="dir-table-container"
|
<div
|
||||||
onChangeCapture={(e) => {
|
ref={refs.setReference}
|
||||||
const event = e as React.ChangeEvent<HTMLInputElement>;
|
className="dir-table-container"
|
||||||
setSearchText(event.target.value.toLowerCase());
|
onChangeCapture={(e) => {
|
||||||
}}
|
const event = e as React.ChangeEvent<HTMLInputElement>;
|
||||||
// onFocusCapture={() => document.getSelection().collapseToEnd()}
|
if (!entryManagerProps) {
|
||||||
>
|
setSearchText(event.target.value.toLowerCase());
|
||||||
<DirectoryTable
|
}
|
||||||
model={model}
|
}}
|
||||||
data={filteredData}
|
{...getReferenceProps()}
|
||||||
search={searchText}
|
onContextMenu={(e) => handleFileContextMenu(e)}
|
||||||
focusIndex={focusIndex}
|
>
|
||||||
setFocusIndex={setFocusIndex}
|
<DirectoryTable
|
||||||
setSearch={setSearchText}
|
model={model}
|
||||||
setSelectedPath={setSelectedPath}
|
data={filteredData}
|
||||||
setRefreshVersion={setRefreshVersion}
|
search={searchText}
|
||||||
/>
|
focusIndex={focusIndex}
|
||||||
</div>
|
setFocusIndex={setFocusIndex}
|
||||||
|
setSearch={setSearchText}
|
||||||
|
setSelectedPath={setSelectedPath}
|
||||||
|
setRefreshVersion={setRefreshVersion}
|
||||||
|
entryManagerOverlayPropsAtom={entryManagerPropsAtom}
|
||||||
|
newFile={newFile}
|
||||||
|
newDirectory={newDirectory}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{entryManagerProps && (
|
||||||
|
<EntryManagerOverlay
|
||||||
|
{...entryManagerProps}
|
||||||
|
forwardRef={refs.setFloating}
|
||||||
|
style={floatingStyles}
|
||||||
|
getReferenceProps={getFloatingProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +87,11 @@ async function initWaveWrap(initOpts: WaveInitOpts) {
|
|||||||
async function reinitWave() {
|
async function reinitWave() {
|
||||||
console.log("Reinit Wave");
|
console.log("Reinit Wave");
|
||||||
getApi().sendLog("Reinit Wave");
|
getApi().sendLog("Reinit Wave");
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.classList.remove("nohover");
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
const client = await WOS.reloadWaveObject<Client>(WOS.makeORef("client", savedInitOpts.clientId));
|
const client = await WOS.reloadWaveObject<Client>(WOS.makeORef("client", savedInitOpts.clientId));
|
||||||
const waveWindow = await WOS.reloadWaveObject<WaveWindow>(WOS.makeORef("window", savedInitOpts.windowId));
|
const waveWindow = await WOS.reloadWaveObject<WaveWindow>(WOS.makeORef("window", savedInitOpts.windowId));
|
||||||
const ws = await WOS.reloadWaveObject<Workspace>(WOS.makeORef("workspace", waveWindow.workspaceid));
|
const ws = await WOS.reloadWaveObject<Workspace>(WOS.makeORef("workspace", waveWindow.workspaceid));
|
||||||
|
@ -13,9 +13,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
WSEvent_ElectronNewWindow = "electron:newwindow"
|
WSEvent_ElectronNewWindow = "electron:newwindow"
|
||||||
WSEvent_ElectronCloseWindow = "electron:closewindow"
|
WSEvent_ElectronCloseWindow = "electron:closewindow"
|
||||||
WSEvent_Rpc = "rpc"
|
WSEvent_ElectronUpdateActiveTab = "electron:updateactivetab"
|
||||||
|
WSEvent_Rpc = "rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WSEventType struct {
|
type WSEventType struct {
|
||||||
|
@ -61,6 +61,33 @@ func (fs *FileService) StatFile(connection string, path string) (*wshrpc.FileInf
|
|||||||
return wshclient.RemoteFileInfoCommand(client, path, &wshrpc.RpcOpts{Route: connRoute})
|
return wshclient.RemoteFileInfoCommand(client, path, &wshrpc.RpcOpts{Route: connRoute})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fs *FileService) Mkdir(connection string, path string) error {
|
||||||
|
if connection == "" {
|
||||||
|
connection = wshrpc.LocalConnName
|
||||||
|
}
|
||||||
|
connRoute := wshutil.MakeConnectionRouteId(connection)
|
||||||
|
client := wshserver.GetMainRpcClient()
|
||||||
|
return wshclient.RemoteMkdirCommand(client, path, &wshrpc.RpcOpts{Route: connRoute})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FileService) TouchFile(connection string, path string) error {
|
||||||
|
if connection == "" {
|
||||||
|
connection = wshrpc.LocalConnName
|
||||||
|
}
|
||||||
|
connRoute := wshutil.MakeConnectionRouteId(connection)
|
||||||
|
client := wshserver.GetMainRpcClient()
|
||||||
|
return wshclient.RemoteFileTouchCommand(client, path, &wshrpc.RpcOpts{Route: connRoute})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FileService) Rename(connection string, path string, newPath string) error {
|
||||||
|
if connection == "" {
|
||||||
|
connection = wshrpc.LocalConnName
|
||||||
|
}
|
||||||
|
connRoute := wshutil.MakeConnectionRouteId(connection)
|
||||||
|
client := wshserver.GetMainRpcClient()
|
||||||
|
return wshclient.RemoteFileRenameCommand(client, [2]string{path, newPath}, &wshrpc.RpcOpts{Route: connRoute})
|
||||||
|
}
|
||||||
|
|
||||||
func (fs *FileService) ReadFile_Meta() tsgenmeta.MethodMeta {
|
func (fs *FileService) ReadFile_Meta() tsgenmeta.MethodMeta {
|
||||||
return tsgenmeta.MethodMeta{
|
return tsgenmeta.MethodMeta{
|
||||||
Desc: "read file",
|
Desc: "read file",
|
||||||
|
@ -121,7 +121,7 @@ func (svc *ObjectService) DeleteBlock(uiContext waveobj.UIContext, blockId strin
|
|||||||
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
||||||
defer cancelFn()
|
defer cancelFn()
|
||||||
ctx = waveobj.ContextWithUpdates(ctx)
|
ctx = waveobj.ContextWithUpdates(ctx)
|
||||||
err := wcore.DeleteBlock(ctx, blockId)
|
err := wcore.DeleteBlock(ctx, blockId, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error deleting block: %w", err)
|
return nil, fmt.Errorf("error deleting block: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -169,21 +169,13 @@ func (svc *WorkspaceService) CloseTab(ctx context.Context, workspaceId string, t
|
|||||||
blockcontroller.StopBlockController(blockId)
|
blockcontroller.StopBlockController(blockId)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
newActiveTabId, err := wcore.DeleteTab(ctx, workspaceId, tabId)
|
newActiveTabId, err := wcore.DeleteTab(ctx, workspaceId, tabId, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("error closing tab: %w", err)
|
return nil, nil, fmt.Errorf("error closing tab: %w", err)
|
||||||
}
|
}
|
||||||
rtn := &CloseTabRtnType{}
|
rtn := &CloseTabRtnType{}
|
||||||
if newActiveTabId == "" {
|
if newActiveTabId == "" {
|
||||||
windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, workspaceId)
|
|
||||||
if err != nil {
|
|
||||||
return rtn, nil, fmt.Errorf("unable to find window for workspace id %v: %w", workspaceId, err)
|
|
||||||
}
|
|
||||||
rtn.CloseWindow = true
|
rtn.CloseWindow = true
|
||||||
err = wcore.CloseWindow(ctx, windowId, fromElectron)
|
|
||||||
if err != nil {
|
|
||||||
return rtn, nil, err
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
rtn.NewActiveTabId = newActiveTabId
|
rtn.NewActiveTabId = newActiveTabId
|
||||||
}
|
}
|
||||||
|
@ -159,6 +159,11 @@ type WorkspaceListEntry struct {
|
|||||||
|
|
||||||
type WorkspaceList []*WorkspaceListEntry
|
type WorkspaceList []*WorkspaceListEntry
|
||||||
|
|
||||||
|
type ActiveTabUpdate struct {
|
||||||
|
WorkspaceId string `json:"workspaceid"`
|
||||||
|
NewActiveTabId string `json:"newactivetabid"`
|
||||||
|
}
|
||||||
|
|
||||||
type Workspace struct {
|
type Workspace struct {
|
||||||
OID string `json:"oid"`
|
OID string `json:"oid"`
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
|
@ -3,11 +3,14 @@ package wcore
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
|
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
|
||||||
"github.com/wavetermdev/waveterm/pkg/panichandler"
|
"github.com/wavetermdev/waveterm/pkg/panichandler"
|
||||||
"github.com/wavetermdev/waveterm/pkg/telemetry"
|
"github.com/wavetermdev/waveterm/pkg/telemetry"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
||||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||||
"github.com/wavetermdev/waveterm/pkg/wps"
|
"github.com/wavetermdev/waveterm/pkg/wps"
|
||||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||||
@ -21,13 +24,34 @@ func CreateSubBlock(ctx context.Context, blockId string, blockDef *waveobj.Block
|
|||||||
if blockDef.Meta == nil || blockDef.Meta.GetString(waveobj.MetaKey_View, "") == "" {
|
if blockDef.Meta == nil || blockDef.Meta.GetString(waveobj.MetaKey_View, "") == "" {
|
||||||
return nil, fmt.Errorf("no view provided for new block")
|
return nil, fmt.Errorf("no view provided for new block")
|
||||||
}
|
}
|
||||||
blockData, err := wstore.CreateSubBlock(ctx, blockId, blockDef)
|
blockData, err := createSubBlockObj(ctx, blockId, blockDef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating sub block: %w", err)
|
return nil, fmt.Errorf("error creating sub block: %w", err)
|
||||||
}
|
}
|
||||||
return blockData, nil
|
return blockData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createSubBlockObj(ctx context.Context, parentBlockId string, blockDef *waveobj.BlockDef) (*waveobj.Block, error) {
|
||||||
|
return wstore.WithTxRtn(ctx, func(tx *wstore.TxWrap) (*waveobj.Block, error) {
|
||||||
|
parentBlock, _ := wstore.DBGet[*waveobj.Block](tx.Context(), parentBlockId)
|
||||||
|
if parentBlock == nil {
|
||||||
|
return nil, fmt.Errorf("parent block not found: %q", parentBlockId)
|
||||||
|
}
|
||||||
|
blockId := uuid.NewString()
|
||||||
|
blockData := &waveobj.Block{
|
||||||
|
OID: blockId,
|
||||||
|
ParentORef: waveobj.MakeORef(waveobj.OType_Block, parentBlockId).String(),
|
||||||
|
BlockDef: blockDef,
|
||||||
|
RuntimeOpts: nil,
|
||||||
|
Meta: blockDef.Meta,
|
||||||
|
}
|
||||||
|
wstore.DBInsert(tx.Context(), blockData)
|
||||||
|
parentBlock.SubBlockIds = append(parentBlock.SubBlockIds, blockId)
|
||||||
|
wstore.DBUpdate(tx.Context(), parentBlock)
|
||||||
|
return blockData, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.Block, error) {
|
func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.Block, error) {
|
||||||
if blockDef == nil {
|
if blockDef == nil {
|
||||||
return nil, fmt.Errorf("blockDef is nil")
|
return nil, fmt.Errorf("blockDef is nil")
|
||||||
@ -35,7 +59,7 @@ func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef,
|
|||||||
if blockDef.Meta == nil || blockDef.Meta.GetString(waveobj.MetaKey_View, "") == "" {
|
if blockDef.Meta == nil || blockDef.Meta.GetString(waveobj.MetaKey_View, "") == "" {
|
||||||
return nil, fmt.Errorf("no view provided for new block")
|
return nil, fmt.Errorf("no view provided for new block")
|
||||||
}
|
}
|
||||||
blockData, err := wstore.CreateBlock(ctx, tabId, blockDef, rtOpts)
|
blockData, err := createBlockObj(ctx, tabId, blockDef, rtOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating block: %w", err)
|
return nil, fmt.Errorf("error creating block: %w", err)
|
||||||
}
|
}
|
||||||
@ -54,7 +78,32 @@ func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef,
|
|||||||
return blockData, nil
|
return blockData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteBlock(ctx context.Context, blockId string) error {
|
func createBlockObj(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.Block, error) {
|
||||||
|
return wstore.WithTxRtn(ctx, func(tx *wstore.TxWrap) (*waveobj.Block, error) {
|
||||||
|
tab, _ := wstore.DBGet[*waveobj.Tab](tx.Context(), tabId)
|
||||||
|
if tab == nil {
|
||||||
|
return nil, fmt.Errorf("tab not found: %q", tabId)
|
||||||
|
}
|
||||||
|
blockId := uuid.NewString()
|
||||||
|
blockData := &waveobj.Block{
|
||||||
|
OID: blockId,
|
||||||
|
ParentORef: waveobj.MakeORef(waveobj.OType_Tab, tabId).String(),
|
||||||
|
BlockDef: blockDef,
|
||||||
|
RuntimeOpts: rtOpts,
|
||||||
|
Meta: blockDef.Meta,
|
||||||
|
}
|
||||||
|
wstore.DBInsert(tx.Context(), blockData)
|
||||||
|
tab.BlockIds = append(tab.BlockIds, blockId)
|
||||||
|
wstore.DBUpdate(tx.Context(), tab)
|
||||||
|
return blockData, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must delete all blocks individually first.
|
||||||
|
// Also deletes LayoutState.
|
||||||
|
// recursive: if true, will recursively close parent tab, window, workspace, if they are empty.
|
||||||
|
// Returns new active tab id, error.
|
||||||
|
func DeleteBlock(ctx context.Context, blockId string, recursive bool) error {
|
||||||
block, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId)
|
block, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error getting block: %w", err)
|
return fmt.Errorf("error getting block: %w", err)
|
||||||
@ -64,21 +113,77 @@ func DeleteBlock(ctx context.Context, blockId string) error {
|
|||||||
}
|
}
|
||||||
if len(block.SubBlockIds) > 0 {
|
if len(block.SubBlockIds) > 0 {
|
||||||
for _, subBlockId := range block.SubBlockIds {
|
for _, subBlockId := range block.SubBlockIds {
|
||||||
err := DeleteBlock(ctx, subBlockId)
|
err := DeleteBlock(ctx, subBlockId, recursive)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error deleting subblock %s: %w", subBlockId, err)
|
return fmt.Errorf("error deleting subblock %s: %w", subBlockId, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = wstore.DeleteBlock(ctx, blockId)
|
parentBlockCount, err := deleteBlockObj(ctx, blockId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error deleting block: %w", err)
|
return fmt.Errorf("error deleting block: %w", err)
|
||||||
}
|
}
|
||||||
|
log.Printf("DeleteBlock: parentBlockCount: %d", parentBlockCount)
|
||||||
|
parentORef := waveobj.ParseORefNoErr(block.ParentORef)
|
||||||
|
|
||||||
|
if parentORef.OType == waveobj.OType_Tab {
|
||||||
|
if parentBlockCount == 0 && recursive {
|
||||||
|
// if parent tab has no blocks, delete the tab
|
||||||
|
log.Printf("DeleteBlock: parent tab has no blocks, deleting tab %s", parentORef.OID)
|
||||||
|
parentWorkspaceId, err := wstore.DBFindWorkspaceForTabId(ctx, parentORef.OID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error finding workspace for tab to delete %s: %w", parentORef.OID, err)
|
||||||
|
}
|
||||||
|
newActiveTabId, err := DeleteTab(ctx, parentWorkspaceId, parentORef.OID, true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error deleting tab %s: %w", parentORef.OID, err)
|
||||||
|
}
|
||||||
|
SendActiveTabUpdate(ctx, parentWorkspaceId, newActiveTabId)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
go blockcontroller.StopBlockController(blockId)
|
go blockcontroller.StopBlockController(blockId)
|
||||||
sendBlockCloseEvent(blockId)
|
sendBlockCloseEvent(blockId)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns the updated block count for the parent object
|
||||||
|
func deleteBlockObj(ctx context.Context, blockId string) (int, error) {
|
||||||
|
return wstore.WithTxRtn(ctx, func(tx *wstore.TxWrap) (int, error) {
|
||||||
|
block, err := wstore.DBGet[*waveobj.Block](tx.Context(), blockId)
|
||||||
|
if err != nil {
|
||||||
|
return -1, fmt.Errorf("error getting block: %w", err)
|
||||||
|
}
|
||||||
|
if block == nil {
|
||||||
|
return -1, fmt.Errorf("block not found: %q", blockId)
|
||||||
|
}
|
||||||
|
if len(block.SubBlockIds) > 0 {
|
||||||
|
return -1, fmt.Errorf("block has subblocks, must delete subblocks first")
|
||||||
|
}
|
||||||
|
parentORef := waveobj.ParseORefNoErr(block.ParentORef)
|
||||||
|
parentBlockCount := -1
|
||||||
|
if parentORef != nil {
|
||||||
|
if parentORef.OType == waveobj.OType_Tab {
|
||||||
|
tab, _ := wstore.DBGet[*waveobj.Tab](tx.Context(), parentORef.OID)
|
||||||
|
if tab != nil {
|
||||||
|
tab.BlockIds = utilfn.RemoveElemFromSlice(tab.BlockIds, blockId)
|
||||||
|
wstore.DBUpdate(tx.Context(), tab)
|
||||||
|
parentBlockCount = len(tab.BlockIds)
|
||||||
|
}
|
||||||
|
} else if parentORef.OType == waveobj.OType_Block {
|
||||||
|
parentBlock, _ := wstore.DBGet[*waveobj.Block](tx.Context(), parentORef.OID)
|
||||||
|
if parentBlock != nil {
|
||||||
|
parentBlock.SubBlockIds = utilfn.RemoveElemFromSlice(parentBlock.SubBlockIds, blockId)
|
||||||
|
wstore.DBUpdate(tx.Context(), parentBlock)
|
||||||
|
parentBlockCount = len(parentBlock.SubBlockIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wstore.DBDelete(tx.Context(), waveobj.OType_Block, blockId)
|
||||||
|
return parentBlockCount, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func sendBlockCloseEvent(blockId string) {
|
func sendBlockCloseEvent(blockId string) {
|
||||||
waveEvent := wps.WaveEvent{
|
waveEvent := wps.WaveEvent{
|
||||||
Event: wps.Event_BlockClose,
|
Event: wps.Event_BlockClose,
|
||||||
|
@ -120,6 +120,8 @@ func CreateWindow(ctx context.Context, winSize *waveobj.WinSize, workspaceId str
|
|||||||
return GetWindow(ctx, windowId)
|
return GetWindow(ctx, windowId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CloseWindow closes a window and deletes its workspace if it is empty and not named.
|
||||||
|
// If fromElectron is true, it does not send an event to Electron.
|
||||||
func CloseWindow(ctx context.Context, windowId string, fromElectron bool) error {
|
func CloseWindow(ctx context.Context, windowId string, fromElectron bool) error {
|
||||||
log.Printf("CloseWindow %s\n", windowId)
|
log.Printf("CloseWindow %s\n", windowId)
|
||||||
window, err := GetWindow(ctx, windowId)
|
window, err := GetWindow(ctx, windowId)
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/eventbus"
|
||||||
"github.com/wavetermdev/waveterm/pkg/telemetry"
|
"github.com/wavetermdev/waveterm/pkg/telemetry"
|
||||||
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
||||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||||
@ -27,19 +28,22 @@ func CreateWorkspace(ctx context.Context, name string, icon string, color string
|
|||||||
return ws, nil
|
return ws, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If force is true, it will delete even if workspace is named.
|
||||||
|
// If workspace is empty, it will be deleted, even if it is named.
|
||||||
|
// Returns true if workspace was deleted, false if it was not deleted.
|
||||||
func DeleteWorkspace(ctx context.Context, workspaceId string, force bool) (bool, error) {
|
func DeleteWorkspace(ctx context.Context, workspaceId string, force bool) (bool, error) {
|
||||||
log.Printf("DeleteWorkspace %s\n", workspaceId)
|
log.Printf("DeleteWorkspace %s\n", workspaceId)
|
||||||
workspace, err := wstore.DBMustGet[*waveobj.Workspace](ctx, workspaceId)
|
workspace, err := wstore.DBMustGet[*waveobj.Workspace](ctx, workspaceId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("error getting workspace: %w", err)
|
return false, fmt.Errorf("error getting workspace: %w", err)
|
||||||
}
|
}
|
||||||
if workspace.Name != "" && workspace.Icon != "" && !force {
|
if workspace.Name != "" && workspace.Icon != "" && !force && len(workspace.TabIds) > 0 {
|
||||||
log.Printf("Ignoring DeleteWorkspace for workspace %s as it is named\n", workspaceId)
|
log.Printf("Ignoring DeleteWorkspace for workspace %s as it is named\n", workspaceId)
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
for _, tabId := range workspace.TabIds {
|
for _, tabId := range workspace.TabIds {
|
||||||
log.Printf("deleting tab %s\n", tabId)
|
log.Printf("deleting tab %s\n", tabId)
|
||||||
_, err := DeleteTab(ctx, workspaceId, tabId)
|
_, err := DeleteTab(ctx, workspaceId, tabId, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("error closing tab: %w", err)
|
return false, fmt.Errorf("error closing tab: %w", err)
|
||||||
}
|
}
|
||||||
@ -103,8 +107,9 @@ func CreateTab(ctx context.Context, workspaceId string, tabName string, activate
|
|||||||
|
|
||||||
// Must delete all blocks individually first.
|
// Must delete all blocks individually first.
|
||||||
// Also deletes LayoutState.
|
// Also deletes LayoutState.
|
||||||
|
// recursive: if true, will recursively close parent window, workspace, if they are empty.
|
||||||
// Returns new active tab id, error.
|
// Returns new active tab id, error.
|
||||||
func DeleteTab(ctx context.Context, workspaceId string, tabId string) (string, error) {
|
func DeleteTab(ctx context.Context, workspaceId string, tabId string, recursive bool) (string, error) {
|
||||||
ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId)
|
ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId)
|
||||||
if ws == nil {
|
if ws == nil {
|
||||||
return "", fmt.Errorf("workspace not found: %q", workspaceId)
|
return "", fmt.Errorf("workspace not found: %q", workspaceId)
|
||||||
@ -116,7 +121,7 @@ func DeleteTab(ctx context.Context, workspaceId string, tabId string) (string, e
|
|||||||
|
|
||||||
// close blocks (sends events + stops block controllers)
|
// close blocks (sends events + stops block controllers)
|
||||||
for _, blockId := range tab.BlockIds {
|
for _, blockId := range tab.BlockIds {
|
||||||
err := DeleteBlock(ctx, blockId)
|
err := DeleteBlock(ctx, blockId, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error deleting block %s: %w", blockId, err)
|
return "", fmt.Errorf("error deleting block %s: %w", blockId, err)
|
||||||
}
|
}
|
||||||
@ -139,6 +144,17 @@ func DeleteTab(ctx context.Context, workspaceId string, tabId string) (string, e
|
|||||||
wstore.DBUpdate(ctx, ws)
|
wstore.DBUpdate(ctx, ws)
|
||||||
wstore.DBDelete(ctx, waveobj.OType_Tab, tabId)
|
wstore.DBDelete(ctx, waveobj.OType_Tab, tabId)
|
||||||
wstore.DBDelete(ctx, waveobj.OType_LayoutState, tab.LayoutState)
|
wstore.DBDelete(ctx, waveobj.OType_LayoutState, tab.LayoutState)
|
||||||
|
|
||||||
|
if newActiveTabId == "" && recursive {
|
||||||
|
windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, workspaceId)
|
||||||
|
if err != nil {
|
||||||
|
return newActiveTabId, fmt.Errorf("unable to find window for workspace id %v: %w", workspaceId, err)
|
||||||
|
}
|
||||||
|
err = CloseWindow(ctx, windowId, false)
|
||||||
|
if err != nil {
|
||||||
|
return newActiveTabId, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return newActiveTabId, nil
|
return newActiveTabId, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,6 +174,13 @@ func SetActiveTab(ctx context.Context, workspaceId string, tabId string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SendActiveTabUpdate(ctx context.Context, workspaceId string, newActiveTabId string) {
|
||||||
|
eventbus.SendEventToElectron(eventbus.WSEventType{
|
||||||
|
EventType: eventbus.WSEvent_ElectronUpdateActiveTab,
|
||||||
|
Data: &waveobj.ActiveTabUpdate{WorkspaceId: workspaceId, NewActiveTabId: newActiveTabId},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func UpdateWorkspaceTabIds(ctx context.Context, workspaceId string, tabIds []string) error {
|
func UpdateWorkspaceTabIds(ctx context.Context, workspaceId string, tabIds []string) error {
|
||||||
ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId)
|
ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId)
|
||||||
if ws == nil {
|
if ws == nil {
|
||||||
|
@ -259,6 +259,24 @@ func RemoteFileJoinCommand(w *wshutil.WshRpc, data []string, opts *wshrpc.RpcOpt
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// command "remotefilerename", wshserver.RemoteFileRenameCommand
|
||||||
|
func RemoteFileRenameCommand(w *wshutil.WshRpc, data [2]string, opts *wshrpc.RpcOpts) error {
|
||||||
|
_, err := sendRpcRequestCallHelper[any](w, "remotefilerename", data, opts)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// command "remotefiletouch", wshserver.RemoteFileTouchCommand
|
||||||
|
func RemoteFileTouchCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error {
|
||||||
|
_, err := sendRpcRequestCallHelper[any](w, "remotefiletouch", data, opts)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// command "remotemkdir", wshserver.RemoteMkdirCommand
|
||||||
|
func RemoteMkdirCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error {
|
||||||
|
_, err := sendRpcRequestCallHelper[any](w, "remotemkdir", data, opts)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// command "remotestreamcpudata", wshserver.RemoteStreamCpuDataCommand
|
// command "remotestreamcpudata", wshserver.RemoteStreamCpuDataCommand
|
||||||
func RemoteStreamCpuDataCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) chan wshrpc.RespOrErrorUnion[wshrpc.TimeSeriesData] {
|
func RemoteStreamCpuDataCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) chan wshrpc.RespOrErrorUnion[wshrpc.TimeSeriesData] {
|
||||||
return sendRpcRequestResponseStreamHelper[wshrpc.TimeSeriesData](w, "remotestreamcpudata", nil, opts)
|
return sendRpcRequestResponseStreamHelper[wshrpc.TimeSeriesData](w, "remotestreamcpudata", nil, opts)
|
||||||
|
@ -307,6 +307,49 @@ func (impl *ServerImpl) RemoteFileInfoCommand(ctx context.Context, path string)
|
|||||||
return impl.fileInfoInternal(path, true)
|
return impl.fileInfoInternal(path, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (impl *ServerImpl) RemoteFileTouchCommand(ctx context.Context, path string) error {
|
||||||
|
cleanedPath := filepath.Clean(wavebase.ExpandHomeDirSafe(path))
|
||||||
|
if _, err := os.Stat(cleanedPath); err == nil {
|
||||||
|
return fmt.Errorf("file %q already exists", path)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(cleanedPath), 0755); err != nil {
|
||||||
|
return fmt.Errorf("cannot create directory %q: %w", filepath.Dir(cleanedPath), err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(cleanedPath, []byte{}, 0644); err != nil {
|
||||||
|
return fmt.Errorf("cannot create file %q: %w", cleanedPath, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *ServerImpl) RemoteFileRenameCommand(ctx context.Context, pathTuple [2]string) error {
|
||||||
|
path := pathTuple[0]
|
||||||
|
newPath := pathTuple[1]
|
||||||
|
cleanedPath := filepath.Clean(wavebase.ExpandHomeDirSafe(path))
|
||||||
|
cleanedNewPath := filepath.Clean(wavebase.ExpandHomeDirSafe(newPath))
|
||||||
|
if _, err := os.Stat(cleanedNewPath); err == nil {
|
||||||
|
return fmt.Errorf("destination file path %q already exists", path)
|
||||||
|
}
|
||||||
|
if err := os.Rename(cleanedPath, cleanedNewPath); err != nil {
|
||||||
|
return fmt.Errorf("cannot rename file %q to %q: %w", cleanedPath, cleanedNewPath, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *ServerImpl) RemoteMkdirCommand(ctx context.Context, path string) error {
|
||||||
|
cleanedPath := filepath.Clean(wavebase.ExpandHomeDirSafe(path))
|
||||||
|
if stat, err := os.Stat(cleanedPath); err == nil {
|
||||||
|
if stat.IsDir() {
|
||||||
|
return fmt.Errorf("directory %q already exists", path)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("cannot create directory %q, file exists at path", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(cleanedPath, 0755); err != nil {
|
||||||
|
return fmt.Errorf("cannot create directory %q: %w", cleanedPath, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (*ServerImpl) RemoteWriteFileCommand(ctx context.Context, data wshrpc.CommandRemoteWriteFileData) error {
|
func (*ServerImpl) RemoteWriteFileCommand(ctx context.Context, data wshrpc.CommandRemoteWriteFileData) error {
|
||||||
path, err := wavebase.ExpandHomeDir(data.Path)
|
path, err := wavebase.ExpandHomeDir(data.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -61,6 +61,7 @@ const (
|
|||||||
Command_Test = "test"
|
Command_Test = "test"
|
||||||
Command_RemoteStreamFile = "remotestreamfile"
|
Command_RemoteStreamFile = "remotestreamfile"
|
||||||
Command_RemoteFileInfo = "remotefileinfo"
|
Command_RemoteFileInfo = "remotefileinfo"
|
||||||
|
Command_RemoteFileTouch = "remotefiletouch"
|
||||||
Command_RemoteWriteFile = "remotewritefile"
|
Command_RemoteWriteFile = "remotewritefile"
|
||||||
Command_RemoteFileDelete = "remotefiledelete"
|
Command_RemoteFileDelete = "remotefiledelete"
|
||||||
Command_RemoteFileJoin = "remotefilejoin"
|
Command_RemoteFileJoin = "remotefilejoin"
|
||||||
@ -69,6 +70,7 @@ const (
|
|||||||
Command_Activity = "activity"
|
Command_Activity = "activity"
|
||||||
Command_GetVar = "getvar"
|
Command_GetVar = "getvar"
|
||||||
Command_SetVar = "setvar"
|
Command_SetVar = "setvar"
|
||||||
|
Command_RemoteMkdir = "remotemkdir"
|
||||||
|
|
||||||
Command_ConnStatus = "connstatus"
|
Command_ConnStatus = "connstatus"
|
||||||
Command_WslStatus = "wslstatus"
|
Command_WslStatus = "wslstatus"
|
||||||
@ -161,9 +163,12 @@ type WshRpcInterface interface {
|
|||||||
// remotes
|
// remotes
|
||||||
RemoteStreamFileCommand(ctx context.Context, data CommandRemoteStreamFileData) chan RespOrErrorUnion[CommandRemoteStreamFileRtnData]
|
RemoteStreamFileCommand(ctx context.Context, data CommandRemoteStreamFileData) chan RespOrErrorUnion[CommandRemoteStreamFileRtnData]
|
||||||
RemoteFileInfoCommand(ctx context.Context, path string) (*FileInfo, error)
|
RemoteFileInfoCommand(ctx context.Context, path string) (*FileInfo, error)
|
||||||
|
RemoteFileTouchCommand(ctx context.Context, path string) error
|
||||||
|
RemoteFileRenameCommand(ctx context.Context, pathTuple [2]string) error
|
||||||
RemoteFileDeleteCommand(ctx context.Context, path string) error
|
RemoteFileDeleteCommand(ctx context.Context, path string) error
|
||||||
RemoteWriteFileCommand(ctx context.Context, data CommandRemoteWriteFileData) error
|
RemoteWriteFileCommand(ctx context.Context, data CommandRemoteWriteFileData) error
|
||||||
RemoteFileJoinCommand(ctx context.Context, paths []string) (*FileInfo, error)
|
RemoteFileJoinCommand(ctx context.Context, paths []string) (*FileInfo, error)
|
||||||
|
RemoteMkdirCommand(ctx context.Context, path string) error
|
||||||
RemoteStreamCpuDataCommand(ctx context.Context) chan RespOrErrorUnion[TimeSeriesData]
|
RemoteStreamCpuDataCommand(ctx context.Context) chan RespOrErrorUnion[TimeSeriesData]
|
||||||
|
|
||||||
// emain
|
// emain
|
||||||
|
@ -489,7 +489,7 @@ func (ws *WshServer) FileAppendIJsonCommand(ctx context.Context, data wshrpc.Com
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ws *WshServer) DeleteSubBlockCommand(ctx context.Context, data wshrpc.CommandDeleteBlockData) error {
|
func (ws *WshServer) DeleteSubBlockCommand(ctx context.Context, data wshrpc.CommandDeleteBlockData) error {
|
||||||
err := wcore.DeleteBlock(ctx, data.BlockId)
|
err := wcore.DeleteBlock(ctx, data.BlockId, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error deleting block: %w", err)
|
return fmt.Errorf("error deleting block: %w", err)
|
||||||
}
|
}
|
||||||
@ -505,7 +505,7 @@ func (ws *WshServer) DeleteBlockCommand(ctx context.Context, data wshrpc.Command
|
|||||||
if tabId == "" {
|
if tabId == "" {
|
||||||
return fmt.Errorf("no tab found for block")
|
return fmt.Errorf("no tab found for block")
|
||||||
}
|
}
|
||||||
err = wcore.DeleteBlock(ctx, data.BlockId)
|
err = wcore.DeleteBlock(ctx, data.BlockId, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error deleting block: %w", err)
|
return fmt.Errorf("error deleting block: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
||||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||||
)
|
)
|
||||||
@ -32,81 +31,6 @@ func UpdateTabName(ctx context.Context, tabId, name string) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateSubBlock(ctx context.Context, parentBlockId string, blockDef *waveobj.BlockDef) (*waveobj.Block, error) {
|
|
||||||
return WithTxRtn(ctx, func(tx *TxWrap) (*waveobj.Block, error) {
|
|
||||||
parentBlock, _ := DBGet[*waveobj.Block](tx.Context(), parentBlockId)
|
|
||||||
if parentBlock == nil {
|
|
||||||
return nil, fmt.Errorf("parent block not found: %q", parentBlockId)
|
|
||||||
}
|
|
||||||
blockId := uuid.NewString()
|
|
||||||
blockData := &waveobj.Block{
|
|
||||||
OID: blockId,
|
|
||||||
ParentORef: waveobj.MakeORef(waveobj.OType_Block, parentBlockId).String(),
|
|
||||||
BlockDef: blockDef,
|
|
||||||
RuntimeOpts: nil,
|
|
||||||
Meta: blockDef.Meta,
|
|
||||||
}
|
|
||||||
DBInsert(tx.Context(), blockData)
|
|
||||||
parentBlock.SubBlockIds = append(parentBlock.SubBlockIds, blockId)
|
|
||||||
DBUpdate(tx.Context(), parentBlock)
|
|
||||||
return blockData, nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.Block, error) {
|
|
||||||
return WithTxRtn(ctx, func(tx *TxWrap) (*waveobj.Block, error) {
|
|
||||||
tab, _ := DBGet[*waveobj.Tab](tx.Context(), tabId)
|
|
||||||
if tab == nil {
|
|
||||||
return nil, fmt.Errorf("tab not found: %q", tabId)
|
|
||||||
}
|
|
||||||
blockId := uuid.NewString()
|
|
||||||
blockData := &waveobj.Block{
|
|
||||||
OID: blockId,
|
|
||||||
ParentORef: waveobj.MakeORef(waveobj.OType_Tab, tabId).String(),
|
|
||||||
BlockDef: blockDef,
|
|
||||||
RuntimeOpts: rtOpts,
|
|
||||||
Meta: blockDef.Meta,
|
|
||||||
}
|
|
||||||
DBInsert(tx.Context(), blockData)
|
|
||||||
tab.BlockIds = append(tab.BlockIds, blockId)
|
|
||||||
DBUpdate(tx.Context(), tab)
|
|
||||||
return blockData, nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteBlock(ctx context.Context, blockId string) error {
|
|
||||||
return WithTx(ctx, func(tx *TxWrap) error {
|
|
||||||
block, err := DBGet[*waveobj.Block](tx.Context(), blockId)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error getting block: %w", err)
|
|
||||||
}
|
|
||||||
if block == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if len(block.SubBlockIds) > 0 {
|
|
||||||
return fmt.Errorf("block has subblocks, must delete subblocks first")
|
|
||||||
}
|
|
||||||
parentORef := waveobj.ParseORefNoErr(block.ParentORef)
|
|
||||||
if parentORef != nil {
|
|
||||||
if parentORef.OType == waveobj.OType_Tab {
|
|
||||||
tab, _ := DBGet[*waveobj.Tab](tx.Context(), parentORef.OID)
|
|
||||||
if tab != nil {
|
|
||||||
tab.BlockIds = utilfn.RemoveElemFromSlice(tab.BlockIds, blockId)
|
|
||||||
DBUpdate(tx.Context(), tab)
|
|
||||||
}
|
|
||||||
} else if parentORef.OType == waveobj.OType_Block {
|
|
||||||
parentBlock, _ := DBGet[*waveobj.Block](tx.Context(), parentORef.OID)
|
|
||||||
if parentBlock != nil {
|
|
||||||
parentBlock.SubBlockIds = utilfn.RemoveElemFromSlice(parentBlock.SubBlockIds, blockId)
|
|
||||||
DBUpdate(tx.Context(), parentBlock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DBDelete(tx.Context(), waveobj.OType_Block, blockId)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// must delete all blocks individually first
|
// must delete all blocks individually first
|
||||||
// also deletes LayoutState
|
// also deletes LayoutState
|
||||||
func DeleteTab(ctx context.Context, workspaceId string, tabId string) error {
|
func DeleteTab(ctx context.Context, workspaceId string, tabId string) error {
|
||||||
|
Loading…
Reference in New Issue
Block a user