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
|
||||
SendCommand(blockid: string, cmd: BlockCommand): Promise<void> {
|
||||
SendCommand(cmd: string, arg3: BlockCommand): Promise<void> {
|
||||
return WOS.callBackendService("block", "SendCommand", Array.from(arguments))
|
||||
}
|
||||
}
|
||||
@ -109,6 +109,11 @@ class ObjectServiceType {
|
||||
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
|
||||
UpdateWorkspaceTabIds(workspaceId: string, tabIds: string[]): Promise<void> {
|
||||
return WOS.callBackendService("object", "UpdateWorkspaceTabIds", Array.from(arguments))
|
||||
|
@ -32,6 +32,13 @@
|
||||
transform: translate(-50%, -50%);
|
||||
-webkit-user-select: none;
|
||||
z-index: 3;
|
||||
|
||||
&.focused {
|
||||
outline: none;
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.vertical-line {
|
||||
@ -95,7 +102,7 @@
|
||||
rgba(0, 0, 0, 0.8) 60%,
|
||||
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 * as services from "@/store/services";
|
||||
import * as WOS from "@/store/wos";
|
||||
import { clsx } from "clsx";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { forwardRef, useEffect, useRef, useState } from "react";
|
||||
|
||||
import "./tab.less";
|
||||
|
||||
@ -14,18 +12,84 @@ interface TabProps {
|
||||
isBeforeActive: boolean;
|
||||
isDragging: boolean;
|
||||
onSelect: () => void;
|
||||
onClose: () => void;
|
||||
onDragStart: (event: MouseEvent) => void;
|
||||
onClose: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
onDragStart: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
onLoaded: () => void;
|
||||
}
|
||||
|
||||
const Tab = React.forwardRef<HTMLDivElement, TabProps>(
|
||||
const Tab = forwardRef<HTMLDivElement, TabProps>(
|
||||
({ id, active, isBeforeActive, isDragging, onLoaded, onSelect, onClose, onDragStart }, ref) => {
|
||||
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);
|
||||
|
||||
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(() => {
|
||||
if (!loadedRef.current) {
|
||||
onLoaded();
|
||||
@ -33,6 +97,11 @@ const Tab = React.forwardRef<HTMLDivElement, TabProps>(
|
||||
}
|
||||
}, [onLoaded]);
|
||||
|
||||
// Prevent drag from being triggered on mousedown
|
||||
const handleMouseDownOnClose = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
@ -41,10 +110,20 @@ const Tab = React.forwardRef<HTMLDivElement, TabProps>(
|
||||
onClick={onSelect}
|
||||
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" />}
|
||||
{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" />
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -425,7 +425,8 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
||||
}, 30);
|
||||
};
|
||||
|
||||
const handleCloseTab = (tabId: string) => {
|
||||
const handleCloseTab = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>, tabId: string) => {
|
||||
event.stopPropagation();
|
||||
services.WindowService.CloseTab(tabId);
|
||||
deleteLayoutStateAtomForTab(tabId);
|
||||
};
|
||||
@ -458,7 +459,7 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
||||
onSelect={() => handleSelectTab(tabId)}
|
||||
active={activetabid === tabId}
|
||||
onDragStart={(event) => handleDragStart(event, tabId, tabRefs.current[index])}
|
||||
onClose={() => handleCloseTab(tabId)}
|
||||
onClose={(event) => handleCloseTab(event, tabId)}
|
||||
onLoaded={() => handleTabLoaded(tabId)}
|
||||
isBeforeActive={isBeforeActive(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 = {
|
||||
command: string;
|
||||
} & ( ResolveIdsCommand | BlockSetViewCommand | BlockSetMetaCommand | BlockGetMetaCommand | BlockMessageCommand | BlockInputCommand | BlockAppendFileCommand | BlockAppendIJsonCommand | CreateBlockCommand );
|
||||
} & ( CreateBlockCommand | BlockInputCommand | BlockAppendFileCommand | ResolveIdsCommand | BlockMessageCommand | BlockAppendIJsonCommand | BlockSetViewCommand | BlockSetMetaCommand | BlockGetMetaCommand );
|
||||
|
||||
// wstore.BlockDef
|
||||
type BlockDef = {
|
||||
@ -49,6 +49,7 @@ declare global {
|
||||
|
||||
// wshutil.BlockInputCommand
|
||||
type BlockInputCommand = {
|
||||
blockid: string;
|
||||
command: "controller:input";
|
||||
inputdata64?: string;
|
||||
signame?: string;
|
||||
@ -91,6 +92,7 @@ declare global {
|
||||
// wshutil.CreateBlockCommand
|
||||
type CreateBlockCommand = {
|
||||
command: "createblock";
|
||||
tabid: string;
|
||||
blockdef: BlockDef;
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
return tsgenmeta.MethodMeta{
|
||||
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) {
|
||||
return WithTxRtn(ctx, func(tx *TxWrap) (*Block, error) {
|
||||
tab, _ := DBGet[*Tab](tx.Context(), tabId)
|
||||
|
Loading…
Reference in New Issue
Block a user