mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-21 21:32:13 +01:00
Add ability to edit tab name (#67)
This commit is contained in:
parent
2c6f6d917f
commit
9cc5d9d3ae
@ -12,7 +12,7 @@ class BlockServiceType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// send command to block
|
// send command to block
|
||||||
SendCommand(blockid: string, cmd: BlockCommand): Promise<void> {
|
SendCommand(cmd: string, arg3: BlockCommand): Promise<void> {
|
||||||
return WOS.callBackendService("block", "SendCommand", Array.from(arguments))
|
return WOS.callBackendService("block", "SendCommand", Array.from(arguments))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,6 +109,11 @@ class ObjectServiceType {
|
|||||||
return WOS.callBackendService("object", "UpdateObjectMeta", Array.from(arguments))
|
return WOS.callBackendService("object", "UpdateObjectMeta", Array.from(arguments))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @returns object updates
|
||||||
|
UpdateTabName(tabId: string, name: string): Promise<void> {
|
||||||
|
return WOS.callBackendService("object", "UpdateTabName", Array.from(arguments))
|
||||||
|
}
|
||||||
|
|
||||||
// @returns object updates
|
// @returns object updates
|
||||||
UpdateWorkspaceTabIds(workspaceId: string, tabIds: string[]): Promise<void> {
|
UpdateWorkspaceTabIds(workspaceId: string, tabIds: string[]): Promise<void> {
|
||||||
return WOS.callBackendService("object", "UpdateWorkspaceTabIds", Array.from(arguments))
|
return WOS.callBackendService("object", "UpdateWorkspaceTabIds", Array.from(arguments))
|
||||||
|
@ -32,6 +32,13 @@
|
|||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
|
|
||||||
|
&.focused {
|
||||||
|
outline: none;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.vertical-line {
|
.vertical-line {
|
||||||
@ -95,7 +102,7 @@
|
|||||||
rgba(0, 0, 0, 0.8) 60%,
|
rgba(0, 0, 0, 0.8) 60%,
|
||||||
rgba(0, 0, 0, 0.7) 100%
|
rgba(0, 0, 0, 0.7) 100%
|
||||||
);
|
);
|
||||||
pointer-events: none; /* Prevents the background from capturing mouse events */
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
import { Button } from "@/element/button";
|
import { Button } from "@/element/button";
|
||||||
|
import * as services from "@/store/services";
|
||||||
import * as WOS from "@/store/wos";
|
import * as WOS from "@/store/wos";
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import React, { useEffect, useRef } from "react";
|
import { forwardRef, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import "./tab.less";
|
import "./tab.less";
|
||||||
|
|
||||||
@ -14,18 +12,84 @@ interface TabProps {
|
|||||||
isBeforeActive: boolean;
|
isBeforeActive: boolean;
|
||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
onClose: () => void;
|
onClose: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||||
onDragStart: (event: MouseEvent) => void;
|
onDragStart: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||||
onLoaded: () => void;
|
onLoaded: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Tab = React.forwardRef<HTMLDivElement, TabProps>(
|
const Tab = forwardRef<HTMLDivElement, TabProps>(
|
||||||
({ id, active, isBeforeActive, isDragging, onLoaded, onSelect, onClose, onDragStart }, ref) => {
|
({ id, active, isBeforeActive, isDragging, onLoaded, onSelect, onClose, onDragStart }, ref) => {
|
||||||
const [tabData, tabLoading] = WOS.useWaveObjectValue<Tab>(WOS.makeORef("tab", id));
|
const [tabData, tabLoading] = WOS.useWaveObjectValue<Tab>(WOS.makeORef("tab", id));
|
||||||
const name = tabData?.name ?? "...";
|
const [originalName, setOriginalName] = useState("");
|
||||||
|
const [isEditable, setIsEditable] = useState(false);
|
||||||
|
|
||||||
|
const editableRef = useRef<HTMLDivElement>(null);
|
||||||
|
const editableTimeoutRef = useRef<NodeJS.Timeout>();
|
||||||
const loadedRef = useRef(false);
|
const loadedRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tabData?.name) {
|
||||||
|
setOriginalName(tabData.name);
|
||||||
|
}
|
||||||
|
}, [tabData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (editableTimeoutRef.current) {
|
||||||
|
clearTimeout(editableTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDoubleClick = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
setIsEditable(true);
|
||||||
|
editableTimeoutRef.current = setTimeout(() => {
|
||||||
|
if (editableRef.current) {
|
||||||
|
editableRef.current.focus();
|
||||||
|
document.execCommand("selectAll", false);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
let newText = editableRef.current.innerText.trim();
|
||||||
|
newText = newText || originalName;
|
||||||
|
editableRef.current.innerText = newText;
|
||||||
|
setIsEditable(false);
|
||||||
|
services.ObjectService.UpdateTabName(id, newText);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event) => {
|
||||||
|
if ((event.metaKey || event.ctrlKey) && event.key === "a") {
|
||||||
|
event.preventDefault();
|
||||||
|
if (editableRef.current) {
|
||||||
|
const range = document.createRange();
|
||||||
|
const selection = window.getSelection();
|
||||||
|
range.selectNodeContents(editableRef.current);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
if (editableRef.current.innerText.trim() === "") {
|
||||||
|
editableRef.current.innerText = originalName;
|
||||||
|
}
|
||||||
|
editableRef.current.blur();
|
||||||
|
} else if (event.key === "Escape") {
|
||||||
|
editableRef.current.innerText = originalName;
|
||||||
|
editableRef.current.blur();
|
||||||
|
} else if (
|
||||||
|
editableRef.current.innerText.length >= 8 &&
|
||||||
|
!["Backspace", "Delete", "ArrowLeft", "ArrowRight"].includes(event.key)
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loadedRef.current) {
|
if (!loadedRef.current) {
|
||||||
onLoaded();
|
onLoaded();
|
||||||
@ -33,6 +97,11 @@ const Tab = React.forwardRef<HTMLDivElement, TabProps>(
|
|||||||
}
|
}
|
||||||
}, [onLoaded]);
|
}, [onLoaded]);
|
||||||
|
|
||||||
|
// Prevent drag from being triggered on mousedown
|
||||||
|
const handleMouseDownOnClose = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -41,10 +110,20 @@ const Tab = React.forwardRef<HTMLDivElement, TabProps>(
|
|||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
data-tab-id={id}
|
data-tab-id={id}
|
||||||
>
|
>
|
||||||
<div className="name">{name}</div>
|
<div
|
||||||
|
ref={editableRef}
|
||||||
|
className={clsx("name", { focused: isEditable })}
|
||||||
|
contentEditable={isEditable}
|
||||||
|
onDoubleClick={handleDoubleClick}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
suppressContentEditableWarning={true}
|
||||||
|
>
|
||||||
|
{tabData?.name}
|
||||||
|
</div>
|
||||||
{!isDragging && <div className="vertical-line" />}
|
{!isDragging && <div className="vertical-line" />}
|
||||||
{active && <div className="mask" />}
|
{active && <div className="mask" />}
|
||||||
<Button className="secondary ghost close" onClick={onClose}>
|
<Button className="secondary ghost close" onClick={onClose} onMouseDown={handleMouseDownOnClose}>
|
||||||
<i className="fa fa-solid fa-xmark" />
|
<i className="fa fa-solid fa-xmark" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -425,7 +425,8 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
|||||||
}, 30);
|
}, 30);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseTab = (tabId: string) => {
|
const handleCloseTab = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>, tabId: string) => {
|
||||||
|
event.stopPropagation();
|
||||||
services.WindowService.CloseTab(tabId);
|
services.WindowService.CloseTab(tabId);
|
||||||
deleteLayoutStateAtomForTab(tabId);
|
deleteLayoutStateAtomForTab(tabId);
|
||||||
};
|
};
|
||||||
@ -458,7 +459,7 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
|||||||
onSelect={() => handleSelectTab(tabId)}
|
onSelect={() => handleSelectTab(tabId)}
|
||||||
active={activetabid === tabId}
|
active={activetabid === tabId}
|
||||||
onDragStart={(event) => handleDragStart(event, tabId, tabRefs.current[index])}
|
onDragStart={(event) => handleDragStart(event, tabId, tabRefs.current[index])}
|
||||||
onClose={() => handleCloseTab(tabId)}
|
onClose={(event) => handleCloseTab(event, tabId)}
|
||||||
onLoaded={() => handleTabLoaded(tabId)}
|
onLoaded={() => handleTabLoaded(tabId)}
|
||||||
isBeforeActive={isBeforeActive(tabId)}
|
isBeforeActive={isBeforeActive(tabId)}
|
||||||
isDragging={draggingTab === tabId}
|
isDragging={draggingTab === tabId}
|
||||||
|
6
frontend/types/gotypes.d.ts
vendored
6
frontend/types/gotypes.d.ts
vendored
@ -31,7 +31,7 @@ declare global {
|
|||||||
|
|
||||||
type BlockCommand = {
|
type BlockCommand = {
|
||||||
command: string;
|
command: string;
|
||||||
} & ( ResolveIdsCommand | BlockSetViewCommand | BlockSetMetaCommand | BlockGetMetaCommand | BlockMessageCommand | BlockInputCommand | BlockAppendFileCommand | BlockAppendIJsonCommand | CreateBlockCommand );
|
} & ( CreateBlockCommand | BlockInputCommand | BlockAppendFileCommand | ResolveIdsCommand | BlockMessageCommand | BlockAppendIJsonCommand | BlockSetViewCommand | BlockSetMetaCommand | BlockGetMetaCommand );
|
||||||
|
|
||||||
// wstore.BlockDef
|
// wstore.BlockDef
|
||||||
type BlockDef = {
|
type BlockDef = {
|
||||||
@ -49,6 +49,7 @@ declare global {
|
|||||||
|
|
||||||
// wshutil.BlockInputCommand
|
// wshutil.BlockInputCommand
|
||||||
type BlockInputCommand = {
|
type BlockInputCommand = {
|
||||||
|
blockid: string;
|
||||||
command: "controller:input";
|
command: "controller:input";
|
||||||
inputdata64?: string;
|
inputdata64?: string;
|
||||||
signame?: string;
|
signame?: string;
|
||||||
@ -91,6 +92,7 @@ declare global {
|
|||||||
// wshutil.CreateBlockCommand
|
// wshutil.CreateBlockCommand
|
||||||
type CreateBlockCommand = {
|
type CreateBlockCommand = {
|
||||||
command: "createblock";
|
command: "createblock";
|
||||||
|
tabid: string;
|
||||||
blockdef: BlockDef;
|
blockdef: BlockDef;
|
||||||
rtopts?: RuntimeOpts;
|
rtopts?: RuntimeOpts;
|
||||||
};
|
};
|
||||||
@ -325,4 +327,4 @@ declare global {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {}
|
export {}
|
||||||
|
@ -155,6 +155,23 @@ func (svc *ObjectService) SetActiveTab(uiContext wstore.UIContext, tabId string)
|
|||||||
return updates, nil
|
return updates, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (svc *ObjectService) UpdateTabName_Meta() tsgenmeta.MethodMeta {
|
||||||
|
return tsgenmeta.MethodMeta{
|
||||||
|
ArgNames: []string{"uiContext", "tabId", "name"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ObjectService) UpdateTabName(uiContext wstore.UIContext, tabId, name string) (wstore.UpdatesRtnType, error) {
|
||||||
|
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
||||||
|
defer cancelFn()
|
||||||
|
ctx = wstore.ContextWithUpdates(ctx)
|
||||||
|
err := wstore.UpdateTabName(ctx, tabId, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error updating tab name: %w", err)
|
||||||
|
}
|
||||||
|
return wstore.ContextGetUpdatesRtn(ctx), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (svc *ObjectService) CreateBlock_Meta() tsgenmeta.MethodMeta {
|
func (svc *ObjectService) CreateBlock_Meta() tsgenmeta.MethodMeta {
|
||||||
return tsgenmeta.MethodMeta{
|
return tsgenmeta.MethodMeta{
|
||||||
ArgNames: []string{"uiContext", "blockDef", "rtOpts"},
|
ArgNames: []string{"uiContext", "blockDef", "rtOpts"},
|
||||||
|
@ -211,6 +211,20 @@ func SetActiveTab(ctx context.Context, windowId string, tabId string) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UpdateTabName(ctx context.Context, tabId, name string) error {
|
||||||
|
return WithTx(ctx, func(tx *TxWrap) error {
|
||||||
|
tab, _ := DBGet[*Tab](tx.Context(), tabId)
|
||||||
|
if tab == nil {
|
||||||
|
return fmt.Errorf("tab not found: %q", tabId)
|
||||||
|
}
|
||||||
|
if tabId != "" {
|
||||||
|
tab.Name = name
|
||||||
|
DBUpdate(tx.Context(), tab)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func CreateBlock(ctx context.Context, tabId string, blockDef *BlockDef, rtOpts *RuntimeOpts) (*Block, error) {
|
func CreateBlock(ctx context.Context, tabId string, blockDef *BlockDef, rtOpts *RuntimeOpts) (*Block, error) {
|
||||||
return WithTxRtn(ctx, func(tx *TxWrap) (*Block, error) {
|
return WithTxRtn(ctx, func(tx *TxWrap) (*Block, error) {
|
||||||
tab, _ := DBGet[*Tab](tx.Context(), tabId)
|
tab, _ := DBGet[*Tab](tx.Context(), tabId)
|
||||||
|
Loading…
Reference in New Issue
Block a user