mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
Pinned tabs (#1375)
![image](https://github.com/user-attachments/assets/a4072368-b204-4eed-bb65-8e3884687f9a) This functions very similarly to VSCode's pinned tab feature. To pin a tab, you can right-click on it and select "Pin tab" from the context menu. Once pinned, a tab will be fixed to the left-most edge of the tab bar, in order of pinning. Pinned tabs can be dragged around like any others. If you drag an unpinned tab into the pinned tabs section (any index less than the highest-index pinned tab), it will be pinned. If you drag a pinned tab out of the pinned tab section, it will be unpinned. Pinned tabs' close button is replaced with a persistent pin button, which can be clicked to unpin them. This adds an extra barrier to accidentally closing a pinned tab. They can still be closed from the context menu.
This commit is contained in:
parent
0145e8fe99
commit
aa77b2c259
@ -294,8 +294,8 @@ export class WaveBrowserWindow extends BaseWindow {
|
|||||||
await this.queueTabSwitch(tabView, tabInitialized);
|
await this.queueTabSwitch(tabView, tabInitialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createTab() {
|
async createTab(pinned = false) {
|
||||||
const tabId = await WorkspaceService.CreateTab(this.workspaceId, null, true);
|
const tabId = await WorkspaceService.CreateTab(this.workspaceId, null, true, pinned);
|
||||||
await this.setActiveTab(tabId, false);
|
await this.setActiveTab(tabId, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -625,6 +625,7 @@ function createTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setActiveTab(tabId: string) {
|
function setActiveTab(tabId: string) {
|
||||||
|
// We use this hack to prevent a flicker in the tab bar when switching to a new tab. This class is unset in reinitWave in wave.ts. See tab.scss for where this class is used.
|
||||||
document.body.classList.add("nohover");
|
document.body.classList.add("nohover");
|
||||||
getApi().setActiveTab(tabId);
|
getApi().setActiveTab(tabId);
|
||||||
}
|
}
|
||||||
|
@ -99,15 +99,19 @@ function switchBlockInDirection(tabId: string, direction: NavigateDirection) {
|
|||||||
layoutModel.switchNodeFocusInDirection(direction);
|
layoutModel.switchNodeFocusInDirection(direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAllTabs(ws: Workspace): string[] {
|
||||||
|
return [...(ws.pinnedtabids ?? []), ...(ws.tabids ?? [])];
|
||||||
|
}
|
||||||
|
|
||||||
function switchTabAbs(index: number) {
|
function switchTabAbs(index: number) {
|
||||||
console.log("switchTabAbs", index);
|
console.log("switchTabAbs", index);
|
||||||
const ws = globalStore.get(atoms.workspace);
|
const ws = globalStore.get(atoms.workspace);
|
||||||
const waveWindow = globalStore.get(atoms.waveWindow);
|
|
||||||
const newTabIdx = index - 1;
|
const newTabIdx = index - 1;
|
||||||
if (newTabIdx < 0 || newTabIdx >= ws.tabids.length) {
|
const tabids = getAllTabs(ws);
|
||||||
|
if (newTabIdx < 0 || newTabIdx >= tabids.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newActiveTabId = ws.tabids[newTabIdx];
|
const newActiveTabId = tabids[newTabIdx];
|
||||||
getApi().setActiveTab(newActiveTabId);
|
getApi().setActiveTab(newActiveTabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,8 +120,9 @@ function switchTab(offset: number) {
|
|||||||
const ws = globalStore.get(atoms.workspace);
|
const ws = globalStore.get(atoms.workspace);
|
||||||
const curTabId = globalStore.get(atoms.staticTabId);
|
const curTabId = globalStore.get(atoms.staticTabId);
|
||||||
let tabIdx = -1;
|
let tabIdx = -1;
|
||||||
for (let i = 0; i < ws.tabids.length; i++) {
|
const tabids = getAllTabs(ws);
|
||||||
if (ws.tabids[i] == curTabId) {
|
for (let i = 0; i < tabids.length; i++) {
|
||||||
|
if (tabids[i] == curTabId) {
|
||||||
tabIdx = i;
|
tabIdx = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -125,8 +130,8 @@ function switchTab(offset: number) {
|
|||||||
if (tabIdx == -1) {
|
if (tabIdx == -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newTabIdx = (tabIdx + offset + ws.tabids.length) % ws.tabids.length;
|
const newTabIdx = (tabIdx + offset + tabids.length) % tabids.length;
|
||||||
const newActiveTabId = ws.tabids[newTabIdx];
|
const newActiveTabId = tabids[newTabIdx];
|
||||||
getApi().setActiveTab(newActiveTabId);
|
getApi().setActiveTab(newActiveTabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,7 +246,10 @@ function registerGlobalKeys() {
|
|||||||
});
|
});
|
||||||
globalKeyMap.set("Cmd:w", () => {
|
globalKeyMap.set("Cmd:w", () => {
|
||||||
const tabId = globalStore.get(atoms.staticTabId);
|
const tabId = globalStore.get(atoms.staticTabId);
|
||||||
|
const ws = globalStore.get(atoms.workspace);
|
||||||
|
if (!ws.pinnedtabids?.includes(tabId)) {
|
||||||
genericClose(tabId);
|
genericClose(tabId);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
globalKeyMap.set("Cmd:m", () => {
|
globalKeyMap.set("Cmd:m", () => {
|
||||||
|
@ -168,13 +168,18 @@ export const WindowService = new WindowServiceType();
|
|||||||
|
|
||||||
// workspaceservice.WorkspaceService (workspace)
|
// workspaceservice.WorkspaceService (workspace)
|
||||||
class WorkspaceServiceType {
|
class WorkspaceServiceType {
|
||||||
|
// @returns object updates
|
||||||
|
ChangeTabPinning(workspaceId: string, tabId: string, pinned: boolean): Promise<void> {
|
||||||
|
return WOS.callBackendService("workspace", "ChangeTabPinning", Array.from(arguments))
|
||||||
|
}
|
||||||
|
|
||||||
// @returns object updates
|
// @returns object updates
|
||||||
CloseTab(workspaceId: string, tabId: string, fromElectron: boolean): Promise<CloseTabRtnType> {
|
CloseTab(workspaceId: string, tabId: string, fromElectron: boolean): Promise<CloseTabRtnType> {
|
||||||
return WOS.callBackendService("workspace", "CloseTab", Array.from(arguments))
|
return WOS.callBackendService("workspace", "CloseTab", Array.from(arguments))
|
||||||
}
|
}
|
||||||
|
|
||||||
// @returns tabId (and object updates)
|
// @returns tabId (and object updates)
|
||||||
CreateTab(workspaceId: string, tabName: string, activateTab: boolean): Promise<string> {
|
CreateTab(workspaceId: string, tabName: string, activateTab: boolean, pinned: boolean): Promise<string> {
|
||||||
return WOS.callBackendService("workspace", "CreateTab", Array.from(arguments))
|
return WOS.callBackendService("workspace", "CreateTab", Array.from(arguments))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,7 +200,7 @@ class WorkspaceServiceType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @returns object updates
|
// @returns object updates
|
||||||
UpdateTabIds(workspaceId: string, tabIds: string[]): Promise<void> {
|
UpdateTabIds(workspaceId: string, tabIds: string[], pinnedTabIds: string[]): Promise<void> {
|
||||||
return WOS.callBackendService("workspace", "UpdateTabIds", Array.from(arguments))
|
return WOS.callBackendService("workspace", "UpdateTabIds", Array.from(arguments))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,8 +81,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
.button {
|
||||||
visibility: hidden;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
@ -97,6 +96,10 @@
|
|||||||
padding: 1px 2px;
|
padding: 1px 2px;
|
||||||
transition: none !important;
|
transition: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body:not(.nohover) .tab:hover {
|
body:not(.nohover) .tab:hover {
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import { Button } from "@/element/button";
|
|
||||||
import { ContextMenuModel } from "@/store/contextmenu";
|
|
||||||
import * as services from "@/store/services";
|
|
||||||
import * as WOS from "@/store/wos";
|
|
||||||
import { clsx } from "clsx";
|
|
||||||
import * as React from "react";
|
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
|
|
||||||
|
|
||||||
import { atoms, globalStore, refocusNode } from "@/app/store/global";
|
import { atoms, globalStore, refocusNode } from "@/app/store/global";
|
||||||
import { RpcApi } from "@/app/store/wshclientapi";
|
import { RpcApi } from "@/app/store/wshclientapi";
|
||||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||||
|
import { Button } from "@/element/button";
|
||||||
|
import { ContextMenuModel } from "@/store/contextmenu";
|
||||||
|
import { clsx } from "clsx";
|
||||||
|
import { forwardRef, memo, useEffect, useImperativeHandle, useRef, useState } from "react";
|
||||||
|
import { ObjectService } from "../store/services";
|
||||||
|
import { makeORef, useWaveObjectValue } from "../store/wos";
|
||||||
import "./tab.scss";
|
import "./tab.scss";
|
||||||
|
|
||||||
interface TabProps {
|
interface TabProps {
|
||||||
@ -22,19 +20,21 @@ interface TabProps {
|
|||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
tabWidth: number;
|
tabWidth: number;
|
||||||
isNew: boolean;
|
isNew: boolean;
|
||||||
|
isPinned: boolean;
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
onClose: (event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null) => void;
|
onClose: (event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null) => void;
|
||||||
onDragStart: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
onDragStart: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||||
onLoaded: () => void;
|
onLoaded: () => void;
|
||||||
|
onPinChange: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Tab = React.memo(
|
const Tab = memo(
|
||||||
forwardRef<HTMLDivElement, TabProps>(
|
forwardRef<HTMLDivElement, TabProps>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
active,
|
active,
|
||||||
isFirst,
|
isPinned,
|
||||||
isBeforeActive,
|
isBeforeActive,
|
||||||
isDragging,
|
isDragging,
|
||||||
tabWidth,
|
tabWidth,
|
||||||
@ -43,10 +43,11 @@ const Tab = React.memo(
|
|||||||
onSelect,
|
onSelect,
|
||||||
onClose,
|
onClose,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
|
onPinChange,
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const [tabData, tabLoading] = WOS.useWaveObjectValue<Tab>(WOS.makeORef("tab", id));
|
const [tabData, _] = useWaveObjectValue<Tab>(makeORef("tab", id));
|
||||||
const [originalName, setOriginalName] = useState("");
|
const [originalName, setOriginalName] = useState("");
|
||||||
const [isEditable, setIsEditable] = useState(false);
|
const [isEditable, setIsEditable] = useState(false);
|
||||||
|
|
||||||
@ -87,7 +88,7 @@ const Tab = React.memo(
|
|||||||
newText = newText || originalName;
|
newText = newText || originalName;
|
||||||
editableRef.current.innerText = newText;
|
editableRef.current.innerText = newText;
|
||||||
setIsEditable(false);
|
setIsEditable(false);
|
||||||
services.ObjectService.UpdateTabName(id, newText);
|
ObjectService.UpdateTabName(id, newText);
|
||||||
setTimeout(() => refocusNode(null), 10);
|
setTimeout(() => refocusNode(null), 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -145,7 +146,12 @@ const Tab = React.memo(
|
|||||||
|
|
||||||
function handleContextMenu(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
function handleContextMenu(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let menu: ContextMenuItem[] = [];
|
let menu: ContextMenuItem[] = [
|
||||||
|
{ label: isPinned ? "Unpin Tab" : "Pin Tab", click: onPinChange },
|
||||||
|
{ label: "Rename Tab", click: () => handleRenameTab(null) },
|
||||||
|
{ label: "Copy TabId", click: () => navigator.clipboard.writeText(id) },
|
||||||
|
{ type: "separator" },
|
||||||
|
];
|
||||||
const fullConfig = globalStore.get(atoms.fullConfigAtom);
|
const fullConfig = globalStore.get(atoms.fullConfigAtom);
|
||||||
const bgPresets: string[] = [];
|
const bgPresets: string[] = [];
|
||||||
for (const key in fullConfig?.presets ?? {}) {
|
for (const key in fullConfig?.presets ?? {}) {
|
||||||
@ -158,12 +164,9 @@ const Tab = React.memo(
|
|||||||
const bOrder = fullConfig.presets[b]["display:order"] ?? 0;
|
const bOrder = fullConfig.presets[b]["display:order"] ?? 0;
|
||||||
return aOrder - bOrder;
|
return aOrder - bOrder;
|
||||||
});
|
});
|
||||||
menu.push({ label: "Rename Tab", click: () => handleRenameTab(null) });
|
|
||||||
menu.push({ label: "Copy TabId", click: () => navigator.clipboard.writeText(id) });
|
|
||||||
menu.push({ type: "separator" });
|
|
||||||
if (bgPresets.length > 0) {
|
if (bgPresets.length > 0) {
|
||||||
const submenu: ContextMenuItem[] = [];
|
const submenu: ContextMenuItem[] = [];
|
||||||
const oref = WOS.makeORef("tab", id);
|
const oref = makeORef("tab", id);
|
||||||
for (const presetName of bgPresets) {
|
for (const presetName of bgPresets) {
|
||||||
const preset = fullConfig.presets[presetName];
|
const preset = fullConfig.presets[presetName];
|
||||||
if (preset == null) {
|
if (preset == null) {
|
||||||
@ -172,13 +175,12 @@ const Tab = React.memo(
|
|||||||
submenu.push({
|
submenu.push({
|
||||||
label: preset["display:name"] ?? presetName,
|
label: preset["display:name"] ?? presetName,
|
||||||
click: () => {
|
click: () => {
|
||||||
services.ObjectService.UpdateObjectMeta(oref, preset);
|
ObjectService.UpdateObjectMeta(oref, preset);
|
||||||
RpcApi.ActivityCommand(TabRpcClient, { settabtheme: 1 });
|
RpcApi.ActivityCommand(TabRpcClient, { settabtheme: 1 });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
menu.push({ label: "Backgrounds", type: "submenu", submenu });
|
menu.push({ label: "Backgrounds", type: "submenu", submenu }, { type: "separator" });
|
||||||
menu.push({ type: "separator" });
|
|
||||||
}
|
}
|
||||||
menu.push({ label: "Close Tab", click: () => onClose(null) });
|
menu.push({ label: "Close Tab", click: () => onClose(null) });
|
||||||
ContextMenuModel.showContextMenu(menu, e);
|
ContextMenuModel.showContextMenu(menu, e);
|
||||||
@ -210,9 +212,21 @@ const Tab = React.memo(
|
|||||||
>
|
>
|
||||||
{tabData?.name}
|
{tabData?.name}
|
||||||
</div>
|
</div>
|
||||||
|
{isPinned ? (
|
||||||
|
<Button
|
||||||
|
className="ghost grey pin"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onPinChange();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="fa fa-solid fa-thumbtack" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
<Button className="ghost grey close" onClick={onClose} onMouseDown={handleMouseDownOnClose}>
|
<Button className="ghost grey close" onClick={onClose} onMouseDown={handleMouseDownOnClose}>
|
||||||
<i className="fa fa-solid fa-xmark" />
|
<i className="fa fa-solid fa-xmark" />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -36,9 +36,18 @@
|
|||||||
|
|
||||||
.tab-bar {
|
.tab-bar {
|
||||||
position: relative; // Needed for absolute positioning of child tabs
|
position: relative; // Needed for absolute positioning of child tabs
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
height: 33px;
|
height: 33px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pinned-tab-spacer {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
margin: 2px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
.dev-label,
|
.dev-label,
|
||||||
.app-menu-button {
|
.app-menu-button {
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
|
@ -101,6 +101,7 @@ const ConfigErrorIcon = ({ buttonRef }: { buttonRef: React.RefObject<HTMLElement
|
|||||||
|
|
||||||
const TabBar = memo(({ workspace }: TabBarProps) => {
|
const TabBar = memo(({ workspace }: TabBarProps) => {
|
||||||
const [tabIds, setTabIds] = useState<string[]>([]);
|
const [tabIds, setTabIds] = useState<string[]>([]);
|
||||||
|
const [pinnedTabIds, setPinnedTabIds] = useState<Set<string>>(new Set());
|
||||||
const [dragStartPositions, setDragStartPositions] = useState<number[]>([]);
|
const [dragStartPositions, setDragStartPositions] = useState<number[]>([]);
|
||||||
const [draggingTab, setDraggingTab] = useState<string>();
|
const [draggingTab, setDraggingTab] = useState<string>();
|
||||||
const [tabsLoaded, setTabsLoaded] = useState({});
|
const [tabsLoaded, setTabsLoaded] = useState({});
|
||||||
@ -116,6 +117,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
tabId: "",
|
tabId: "",
|
||||||
ref: { current: null },
|
ref: { current: null },
|
||||||
tabStartX: 0,
|
tabStartX: 0,
|
||||||
|
tabStartIndex: 0,
|
||||||
tabIndex: 0,
|
tabIndex: 0,
|
||||||
initialOffsetX: null,
|
initialOffsetX: null,
|
||||||
totalScrollOffset: null,
|
totalScrollOffset: null,
|
||||||
@ -148,17 +150,25 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (workspace) {
|
if (workspace) {
|
||||||
// Compare current tabIds with new workspace.tabids
|
// Compare current tabIds with new workspace.tabids
|
||||||
const currentTabIds = new Set(tabIds);
|
console.log("tabbar workspace", workspace);
|
||||||
const newTabIds = new Set(workspace.tabids);
|
const newTabIds = new Set([...(workspace.pinnedtabids ?? []), ...(workspace.tabids ?? [])]);
|
||||||
|
const newPinnedTabIds = workspace.pinnedtabids ?? [];
|
||||||
|
|
||||||
const areEqual =
|
const areEqual =
|
||||||
currentTabIds.size === newTabIds.size && [...currentTabIds].every((id) => newTabIds.has(id));
|
tabIds.length === newTabIds.size &&
|
||||||
|
tabIds.every((id) => newTabIds.has(id)) &&
|
||||||
|
newPinnedTabIds.length === pinnedTabIds.size;
|
||||||
|
|
||||||
if (!areEqual) {
|
if (!areEqual) {
|
||||||
setTabIds(workspace.tabids);
|
const newPinnedTabIdSet = new Set(newPinnedTabIds);
|
||||||
|
console.log("newPinnedTabIds", newPinnedTabIds);
|
||||||
|
const newTabIdList = newPinnedTabIds.concat([...newTabIds].filter((id) => !newPinnedTabIdSet.has(id))); // Corrects for any duplicates between the two lists
|
||||||
|
console.log("newTabIdList", newTabIdList);
|
||||||
|
setTabIds(newTabIdList);
|
||||||
|
setPinnedTabIds(newPinnedTabIdSet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [workspace, tabIds]);
|
}, [workspace, tabIds, pinnedTabIds]);
|
||||||
|
|
||||||
const saveTabsPosition = useCallback(() => {
|
const saveTabsPosition = useCallback(() => {
|
||||||
const tabs = tabRefs.current;
|
const tabs = tabRefs.current;
|
||||||
@ -246,9 +256,14 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveTabsPositionDebounced = useCallback(
|
||||||
|
debounce(100, () => saveTabsPosition()),
|
||||||
|
[saveTabsPosition]
|
||||||
|
);
|
||||||
|
|
||||||
const handleResizeTabs = useCallback(() => {
|
const handleResizeTabs = useCallback(() => {
|
||||||
setSizeAndPosition();
|
setSizeAndPosition();
|
||||||
debounce(100, () => saveTabsPosition())();
|
saveTabsPositionDebounced();
|
||||||
}, [tabIds, newTabId, isFullScreen]);
|
}, [tabIds, newTabId, isFullScreen]);
|
||||||
|
|
||||||
const reinitVersion = useAtomValue(atoms.reinitVersion);
|
const reinitVersion = useAtomValue(atoms.reinitVersion);
|
||||||
@ -278,7 +293,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
}, [tabIds, tabsLoaded, newTabId, saveTabsPosition]);
|
}, [tabIds, tabsLoaded, newTabId, saveTabsPosition]);
|
||||||
|
|
||||||
const getDragDirection = (currentX: number) => {
|
const getDragDirection = (currentX: number) => {
|
||||||
let dragDirection;
|
let dragDirection: string;
|
||||||
if (currentX - prevDelta > 0) {
|
if (currentX - prevDelta > 0) {
|
||||||
dragDirection = "+";
|
dragDirection = "+";
|
||||||
} else if (currentX - prevDelta === 0) {
|
} else if (currentX - prevDelta === 0) {
|
||||||
@ -418,6 +433,50 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// } else if ((tabIndex > pinnedTabCount || (tabIndex === 1 && pinnedTabCount === 1)) && isPinned) {
|
||||||
|
|
||||||
|
const setUpdatedTabsDebounced = useCallback(
|
||||||
|
debounce(300, (tabIndex: number, tabIds: string[], pinnedTabIds: Set<string>) => {
|
||||||
|
console.log(
|
||||||
|
"setting updated tabs",
|
||||||
|
tabIds,
|
||||||
|
pinnedTabIds,
|
||||||
|
tabIndex,
|
||||||
|
draggingTabDataRef.current.tabStartIndex
|
||||||
|
);
|
||||||
|
// Reset styles
|
||||||
|
tabRefs.current.forEach((ref) => {
|
||||||
|
ref.current.style.zIndex = "0";
|
||||||
|
ref.current.classList.remove("animate");
|
||||||
|
});
|
||||||
|
let pinnedTabCount = pinnedTabIds.size;
|
||||||
|
const draggedTabId = draggingTabDataRef.current.tabId;
|
||||||
|
const isPinned = pinnedTabIds.has(draggedTabId);
|
||||||
|
if (pinnedTabIds.has(tabIds[tabIndex + 1]) && !isPinned) {
|
||||||
|
pinnedTabIds.add(draggedTabId);
|
||||||
|
} else if (!pinnedTabIds.has(tabIds[tabIndex - 1]) && isPinned) {
|
||||||
|
pinnedTabIds.delete(draggedTabId);
|
||||||
|
}
|
||||||
|
if (pinnedTabCount != pinnedTabIds.size) {
|
||||||
|
console.log("updated pinnedTabIds", pinnedTabIds, tabIds);
|
||||||
|
setPinnedTabIds(pinnedTabIds);
|
||||||
|
pinnedTabCount = pinnedTabIds.size;
|
||||||
|
}
|
||||||
|
// Reset dragging state
|
||||||
|
setDraggingTab(null);
|
||||||
|
// Update workspace tab ids
|
||||||
|
fireAndForget(
|
||||||
|
async () =>
|
||||||
|
await WorkspaceService.UpdateTabIds(
|
||||||
|
workspace.oid,
|
||||||
|
tabIds.slice(pinnedTabCount),
|
||||||
|
tabIds.slice(0, pinnedTabCount)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const handleMouseUp = (event: MouseEvent) => {
|
const handleMouseUp = (event: MouseEvent) => {
|
||||||
const { tabIndex, dragged } = draggingTabDataRef.current;
|
const { tabIndex, dragged } = draggingTabDataRef.current;
|
||||||
|
|
||||||
@ -432,17 +491,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dragged) {
|
if (dragged) {
|
||||||
debounce(300, () => {
|
setUpdatedTabsDebounced(tabIndex, tabIds, pinnedTabIds);
|
||||||
// Reset styles
|
|
||||||
tabRefs.current.forEach((ref) => {
|
|
||||||
ref.current.style.zIndex = "0";
|
|
||||||
ref.current.classList.remove("animate");
|
|
||||||
});
|
|
||||||
// Reset dragging state
|
|
||||||
setDraggingTab(null);
|
|
||||||
// Update workspace tab ids
|
|
||||||
fireAndForget(async () => await WorkspaceService.UpdateTabIds(workspace.oid, tabIds));
|
|
||||||
})();
|
|
||||||
} else {
|
} else {
|
||||||
// Reset styles
|
// Reset styles
|
||||||
tabRefs.current.forEach((ref) => {
|
tabRefs.current.forEach((ref) => {
|
||||||
@ -465,12 +514,14 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
const tabIndex = tabIds.indexOf(tabId);
|
const tabIndex = tabIds.indexOf(tabId);
|
||||||
const tabStartX = dragStartPositions[tabIndex]; // Starting X position of the tab
|
const tabStartX = dragStartPositions[tabIndex]; // Starting X position of the tab
|
||||||
|
|
||||||
|
console.log("handleDragStart", tabId, tabIndex, tabStartX);
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
draggingTabDataRef.current = {
|
draggingTabDataRef.current = {
|
||||||
tabId: ref.current.dataset.tabId,
|
tabId: ref.current.dataset.tabId,
|
||||||
ref,
|
ref,
|
||||||
tabStartX,
|
tabStartX,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
|
tabStartIndex: tabIndex,
|
||||||
initialOffsetX: null,
|
initialOffsetX: null,
|
||||||
totalScrollOffset: 0,
|
totalScrollOffset: 0,
|
||||||
dragged: false,
|
dragged: false,
|
||||||
@ -489,19 +540,31 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddTab = () => {
|
const updateScrollDebounced = useCallback(
|
||||||
createTab();
|
|
||||||
tabsWrapperRef.current.style.transition;
|
|
||||||
tabsWrapperRef.current.style.setProperty("--tabs-wrapper-transition", "width 0.1s ease");
|
|
||||||
|
|
||||||
debounce(30, () => {
|
debounce(30, () => {
|
||||||
if (scrollableRef.current) {
|
if (scrollableRef.current) {
|
||||||
const { viewport } = osInstanceRef.current.elements();
|
const { viewport } = osInstanceRef.current.elements();
|
||||||
viewport.scrollLeft = tabIds.length * tabWidthRef.current;
|
viewport.scrollLeft = tabIds.length * tabWidthRef.current;
|
||||||
}
|
}
|
||||||
})();
|
}),
|
||||||
|
[tabIds]
|
||||||
|
);
|
||||||
|
|
||||||
debounce(100, () => setNewTabId(null))();
|
const setNewTabIdDebounced = useCallback(
|
||||||
|
debounce(100, (tabId: string) => {
|
||||||
|
setNewTabId(tabId);
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAddTab = () => {
|
||||||
|
createTab();
|
||||||
|
tabsWrapperRef.current.style.transition;
|
||||||
|
tabsWrapperRef.current.style.setProperty("--tabs-wrapper-transition", "width 0.1s ease");
|
||||||
|
|
||||||
|
updateScrollDebounced();
|
||||||
|
|
||||||
|
setNewTabIdDebounced(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseTab = (event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null, tabId: string) => {
|
const handleCloseTab = (event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null, tabId: string) => {
|
||||||
@ -511,7 +574,14 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
deleteLayoutModelForTab(tabId);
|
deleteLayoutModelForTab(tabId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTabLoaded = useCallback((tabId) => {
|
const handlePinChange = (tabId: string, pinned: boolean) => {
|
||||||
|
console.log("handlePinChange", tabId, pinned);
|
||||||
|
fireAndForget(async () => {
|
||||||
|
await WorkspaceService.ChangeTabPinning(workspace.oid, tabId, pinned);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTabLoaded = useCallback((tabId: string) => {
|
||||||
setTabsLoaded((prev) => {
|
setTabsLoaded((prev) => {
|
||||||
if (!prev[tabId]) {
|
if (!prev[tabId]) {
|
||||||
// Only update if the tab isn't already marked as loaded
|
// Only update if the tab isn't already marked as loaded
|
||||||
@ -550,17 +620,20 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
<div className="tab-bar" ref={tabBarRef} data-overlayscrollbars-initialize>
|
<div className="tab-bar" ref={tabBarRef} data-overlayscrollbars-initialize>
|
||||||
<div className="tabs-wrapper" ref={tabsWrapperRef} style={{ width: `${tabsWrapperWidth}px` }}>
|
<div className="tabs-wrapper" ref={tabsWrapperRef} style={{ width: `${tabsWrapperWidth}px` }}>
|
||||||
{tabIds.map((tabId, index) => {
|
{tabIds.map((tabId, index) => {
|
||||||
|
const isPinned = pinnedTabIds.has(tabId);
|
||||||
return (
|
return (
|
||||||
<Tab
|
<Tab
|
||||||
key={tabId}
|
key={tabId}
|
||||||
ref={tabRefs.current[index]}
|
ref={tabRefs.current[index]}
|
||||||
id={tabId}
|
id={tabId}
|
||||||
isFirst={index === 0}
|
isFirst={index === 0}
|
||||||
|
isPinned={isPinned}
|
||||||
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={(event) => handleCloseTab(event, tabId)}
|
onClose={(event) => handleCloseTab(event, tabId)}
|
||||||
onLoaded={() => handleTabLoaded(tabId)}
|
onLoaded={() => handleTabLoaded(tabId)}
|
||||||
|
onPinChange={() => handlePinChange(tabId, !isPinned)}
|
||||||
isBeforeActive={isBeforeActive(tabId)}
|
isBeforeActive={isBeforeActive(tabId)}
|
||||||
isDragging={draggingTab === tabId}
|
isDragging={draggingTab === tabId}
|
||||||
tabWidth={tabWidthRef.current}
|
tabWidth={tabWidthRef.current}
|
||||||
|
1
frontend/types/gotypes.d.ts
vendored
1
frontend/types/gotypes.d.ts
vendored
@ -1119,6 +1119,7 @@ declare global {
|
|||||||
icon: string;
|
icon: string;
|
||||||
color: string;
|
color: string;
|
||||||
tabids: string[];
|
tabids: string[];
|
||||||
|
pinnedtabids: string[];
|
||||||
activetabid: string;
|
activetabid: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -87,11 +87,14 @@ 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");
|
||||||
|
|
||||||
|
// We use this hack to prevent a flicker in the tab bar when switching to a new tab. This class is set in setActiveTab in global.ts. See tab.scss for where this class is used.
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.body.classList.remove("nohover");
|
document.body.classList.remove("nohover");
|
||||||
}, 50);
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
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));
|
||||||
|
@ -55,7 +55,7 @@ func (svc *WindowService) CreateWindow(ctx context.Context, winSize *waveobj.Win
|
|||||||
return nil, fmt.Errorf("error getting workspace: %w", err)
|
return nil, fmt.Errorf("error getting workspace: %w", err)
|
||||||
}
|
}
|
||||||
if len(ws.TabIds) == 0 {
|
if len(ws.TabIds) == 0 {
|
||||||
_, err = wcore.CreateTab(ctx, ws.OID, "", true)
|
_, err = wcore.CreateTab(ctx, ws.OID, "", true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return window, fmt.Errorf("error creating tab: %w", err)
|
return window, fmt.Errorf("error creating tab: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package workspaceservice
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
|
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
|
||||||
@ -68,16 +69,16 @@ func (svg *WorkspaceService) ListWorkspaces() (waveobj.WorkspaceList, error) {
|
|||||||
|
|
||||||
func (svc *WorkspaceService) CreateTab_Meta() tsgenmeta.MethodMeta {
|
func (svc *WorkspaceService) CreateTab_Meta() tsgenmeta.MethodMeta {
|
||||||
return tsgenmeta.MethodMeta{
|
return tsgenmeta.MethodMeta{
|
||||||
ArgNames: []string{"workspaceId", "tabName", "activateTab"},
|
ArgNames: []string{"workspaceId", "tabName", "activateTab", "pinned"},
|
||||||
ReturnDesc: "tabId",
|
ReturnDesc: "tabId",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *WorkspaceService) CreateTab(workspaceId string, tabName string, activateTab bool) (string, waveobj.UpdatesRtnType, error) {
|
func (svc *WorkspaceService) CreateTab(workspaceId string, tabName string, activateTab bool, pinned bool) (string, waveobj.UpdatesRtnType, error) {
|
||||||
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)
|
||||||
tabId, err := wcore.CreateTab(ctx, workspaceId, tabName, activateTab)
|
tabId, err := wcore.CreateTab(ctx, workspaceId, tabName, activateTab, pinned)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("error creating tab: %w", err)
|
return "", nil, fmt.Errorf("error creating tab: %w", err)
|
||||||
}
|
}
|
||||||
@ -93,17 +94,39 @@ func (svc *WorkspaceService) CreateTab(workspaceId string, tabName string, activ
|
|||||||
return tabId, updates, nil
|
return tabId, updates, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *WorkspaceService) UpdateTabIds_Meta() tsgenmeta.MethodMeta {
|
func (svc *WorkspaceService) ChangeTabPinning_Meta() tsgenmeta.MethodMeta {
|
||||||
return tsgenmeta.MethodMeta{
|
return tsgenmeta.MethodMeta{
|
||||||
ArgNames: []string{"uiContext", "workspaceId", "tabIds"},
|
ArgNames: []string{"ctx", "workspaceId", "tabId", "pinned"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *WorkspaceService) UpdateTabIds(uiContext waveobj.UIContext, workspaceId string, tabIds []string) (waveobj.UpdatesRtnType, error) {
|
func (svc *WorkspaceService) ChangeTabPinning(ctx context.Context, workspaceId string, tabId string, pinned bool) (waveobj.UpdatesRtnType, error) {
|
||||||
|
log.Printf("ChangeTabPinning %s %s %v\n", workspaceId, tabId, pinned)
|
||||||
|
ctx = waveobj.ContextWithUpdates(ctx)
|
||||||
|
err := wcore.ChangeTabPinning(ctx, workspaceId, tabId, pinned)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error toggling tab pinning: %w", err)
|
||||||
|
}
|
||||||
|
updates := waveobj.ContextGetUpdatesRtn(ctx)
|
||||||
|
go func() {
|
||||||
|
defer panichandler.PanicHandler("WorkspaceService:ChangeTabPinning:SendUpdateEvents")
|
||||||
|
wps.Broker.SendUpdateEvents(updates)
|
||||||
|
}()
|
||||||
|
return updates, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *WorkspaceService) UpdateTabIds_Meta() tsgenmeta.MethodMeta {
|
||||||
|
return tsgenmeta.MethodMeta{
|
||||||
|
ArgNames: []string{"uiContext", "workspaceId", "tabIds", "pinnedTabIds"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *WorkspaceService) UpdateTabIds(uiContext waveobj.UIContext, workspaceId string, tabIds []string, pinnedTabIds []string) (waveobj.UpdatesRtnType, error) {
|
||||||
|
log.Printf("UpdateTabIds %s %v %v\n", workspaceId, tabIds, pinnedTabIds)
|
||||||
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.UpdateWorkspaceTabIds(ctx, workspaceId, tabIds)
|
err := wcore.UpdateWorkspaceTabIds(ctx, workspaceId, tabIds, pinnedTabIds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error updating workspace tab ids: %w", err)
|
return nil, fmt.Errorf("error updating workspace tab ids: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -171,6 +171,7 @@ type Workspace struct {
|
|||||||
Icon string `json:"icon"`
|
Icon string `json:"icon"`
|
||||||
Color string `json:"color"`
|
Color string `json:"color"`
|
||||||
TabIds []string `json:"tabids"`
|
TabIds []string `json:"tabids"`
|
||||||
|
PinnedTabIds []string `json:"pinnedtabids"`
|
||||||
ActiveTabId string `json:"activetabid"`
|
ActiveTabId string `json:"activetabid"`
|
||||||
Meta MetaMapType `json:"meta"`
|
Meta MetaMapType `json:"meta"`
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ func EnsureInitialData() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating default workspace: %w", err)
|
return fmt.Errorf("error creating default workspace: %w", err)
|
||||||
}
|
}
|
||||||
_, err = CreateTab(ctx, defaultWs.OID, "", true)
|
_, err = CreateTab(ctx, defaultWs.OID, "", true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating tab: %w", err)
|
return fmt.Errorf("error creating tab: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -176,7 +176,7 @@ func CheckAndFixWindow(ctx context.Context, windowId string) *waveobj.Window {
|
|||||||
}
|
}
|
||||||
if len(ws.TabIds) == 0 {
|
if len(ws.TabIds) == 0 {
|
||||||
log.Printf("fixing workspace with no tabs %q (in checkAndFixWindow)\n", ws.OID)
|
log.Printf("fixing workspace with no tabs %q (in checkAndFixWindow)\n", ws.OID)
|
||||||
_, err = CreateTab(ctx, ws.OID, "", true)
|
_, err = CreateTab(ctx, ws.OID, "", true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error creating tab (in checkAndFixWindow): %v\n", err)
|
log.Printf("error creating tab (in checkAndFixWindow): %v\n", err)
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ func CreateWorkspace(ctx context.Context, name string, icon string, color string
|
|||||||
ws := &waveobj.Workspace{
|
ws := &waveobj.Workspace{
|
||||||
OID: uuid.NewString(),
|
OID: uuid.NewString(),
|
||||||
TabIds: []string{},
|
TabIds: []string{},
|
||||||
|
PinnedTabIds: []string{},
|
||||||
Name: name,
|
Name: name,
|
||||||
Icon: icon,
|
Icon: icon,
|
||||||
Color: color,
|
Color: color,
|
||||||
@ -37,11 +38,13 @@ func DeleteWorkspace(ctx context.Context, workspaceId string, force bool) (bool,
|
|||||||
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 && len(workspace.TabIds) > 0 {
|
if workspace.Name != "" && workspace.Icon != "" && !force && len(workspace.TabIds) > 0 && len(workspace.PinnedTabIds) > 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 {
|
|
||||||
|
// delete all pinned and unpinned tabs
|
||||||
|
for _, tabId := range append(workspace.TabIds, workspace.PinnedTabIds...) {
|
||||||
log.Printf("deleting tab %s\n", tabId)
|
log.Printf("deleting tab %s\n", tabId)
|
||||||
_, err := DeleteTab(ctx, workspaceId, tabId, false)
|
_, err := DeleteTab(ctx, workspaceId, tabId, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -60,7 +63,30 @@ func GetWorkspace(ctx context.Context, wsID string) (*waveobj.Workspace, error)
|
|||||||
return wstore.DBMustGet[*waveobj.Workspace](ctx, wsID)
|
return wstore.DBMustGet[*waveobj.Workspace](ctx, wsID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTabObj(ctx context.Context, workspaceId string, name string) (*waveobj.Tab, error) {
|
// returns tabid
|
||||||
|
func CreateTab(ctx context.Context, workspaceId string, tabName string, activateTab bool, pinned bool) (string, error) {
|
||||||
|
if tabName == "" {
|
||||||
|
ws, err := GetWorkspace(ctx, workspaceId)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("workspace %s not found: %w", workspaceId, err)
|
||||||
|
}
|
||||||
|
tabName = "T" + fmt.Sprint(len(ws.TabIds)+1)
|
||||||
|
}
|
||||||
|
tab, err := createTabObj(ctx, workspaceId, tabName, pinned)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error creating tab: %w", err)
|
||||||
|
}
|
||||||
|
if activateTab {
|
||||||
|
err = SetActiveTab(ctx, workspaceId, tab.OID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error setting active tab: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{NewTab: 1}, "createtab")
|
||||||
|
return tab.OID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTabObj(ctx context.Context, workspaceId string, name string, pinned bool) (*waveobj.Tab, error) {
|
||||||
ws, err := GetWorkspace(ctx, workspaceId)
|
ws, err := GetWorkspace(ctx, workspaceId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("workspace %s not found: %w", workspaceId, err)
|
return nil, fmt.Errorf("workspace %s not found: %w", workspaceId, err)
|
||||||
@ -75,36 +101,17 @@ func createTabObj(ctx context.Context, workspaceId string, name string) (*waveob
|
|||||||
layoutState := &waveobj.LayoutState{
|
layoutState := &waveobj.LayoutState{
|
||||||
OID: layoutStateId,
|
OID: layoutStateId,
|
||||||
}
|
}
|
||||||
|
if pinned {
|
||||||
|
ws.PinnedTabIds = append(ws.PinnedTabIds, tab.OID)
|
||||||
|
} else {
|
||||||
ws.TabIds = append(ws.TabIds, tab.OID)
|
ws.TabIds = append(ws.TabIds, tab.OID)
|
||||||
|
}
|
||||||
wstore.DBInsert(ctx, tab)
|
wstore.DBInsert(ctx, tab)
|
||||||
wstore.DBInsert(ctx, layoutState)
|
wstore.DBInsert(ctx, layoutState)
|
||||||
wstore.DBUpdate(ctx, ws)
|
wstore.DBUpdate(ctx, ws)
|
||||||
return tab, nil
|
return tab, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns tabid
|
|
||||||
func CreateTab(ctx context.Context, workspaceId string, tabName string, activateTab bool) (string, error) {
|
|
||||||
if tabName == "" {
|
|
||||||
ws, err := GetWorkspace(ctx, workspaceId)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("workspace %s not found: %w", workspaceId, err)
|
|
||||||
}
|
|
||||||
tabName = "T" + fmt.Sprint(len(ws.TabIds)+1)
|
|
||||||
}
|
|
||||||
tab, err := createTabObj(ctx, workspaceId, tabName)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error creating tab: %w", err)
|
|
||||||
}
|
|
||||||
if activateTab {
|
|
||||||
err = SetActiveTab(ctx, workspaceId, tab.OID)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error setting active tab: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{NewTab: 1}, "createtab")
|
|
||||||
return tab.OID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
// recursive: if true, will recursively close parent window, workspace, if they are empty.
|
||||||
@ -114,38 +121,50 @@ func DeleteTab(ctx context.Context, workspaceId string, tabId string, recursive
|
|||||||
if ws == nil {
|
if ws == nil {
|
||||||
return "", fmt.Errorf("workspace not found: %q", workspaceId)
|
return "", fmt.Errorf("workspace not found: %q", workspaceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensure tab is in workspace
|
||||||
|
tabIdx := utilfn.FindStringInSlice(ws.TabIds, tabId)
|
||||||
|
tabIdxPinned := utilfn.FindStringInSlice(ws.PinnedTabIds, tabId)
|
||||||
|
if tabIdx != -1 {
|
||||||
|
ws.TabIds = append(ws.TabIds[:tabIdx], ws.TabIds[tabIdx+1:]...)
|
||||||
|
} else if tabIdxPinned != -1 {
|
||||||
|
ws.PinnedTabIds = append(ws.PinnedTabIds[:tabIdxPinned], ws.PinnedTabIds[tabIdxPinned+1:]...)
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("tab %s not found in workspace %s", tabId, workspaceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// close blocks (sends events + stops block controllers)
|
||||||
tab, _ := wstore.DBGet[*waveobj.Tab](ctx, tabId)
|
tab, _ := wstore.DBGet[*waveobj.Tab](ctx, tabId)
|
||||||
if tab == nil {
|
if tab == nil {
|
||||||
return "", fmt.Errorf("tab not found: %q", tabId)
|
return "", fmt.Errorf("tab not found: %q", tabId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// close blocks (sends events + stops block controllers)
|
|
||||||
for _, blockId := range tab.BlockIds {
|
for _, blockId := range tab.BlockIds {
|
||||||
err := DeleteBlock(ctx, blockId, false)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tabIdx := utilfn.FindStringInSlice(ws.TabIds, tabId)
|
|
||||||
if tabIdx == -1 {
|
// if the tab is active, determine new active tab
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
ws.TabIds = append(ws.TabIds[:tabIdx], ws.TabIds[tabIdx+1:]...)
|
|
||||||
newActiveTabId := ws.ActiveTabId
|
newActiveTabId := ws.ActiveTabId
|
||||||
if len(ws.TabIds) > 0 {
|
|
||||||
if ws.ActiveTabId == tabId {
|
if ws.ActiveTabId == tabId {
|
||||||
|
if len(ws.TabIds) > 0 && tabIdx != -1 {
|
||||||
newActiveTabId = ws.TabIds[max(0, min(tabIdx-1, len(ws.TabIds)-1))]
|
newActiveTabId = ws.TabIds[max(0, min(tabIdx-1, len(ws.TabIds)-1))]
|
||||||
}
|
} else if len(ws.PinnedTabIds) > 0 {
|
||||||
|
newActiveTabId = ws.PinnedTabIds[0]
|
||||||
} else {
|
} else {
|
||||||
newActiveTabId = ""
|
newActiveTabId = ""
|
||||||
}
|
}
|
||||||
|
}
|
||||||
ws.ActiveTabId = newActiveTabId
|
ws.ActiveTabId = newActiveTabId
|
||||||
|
|
||||||
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 no tabs remaining, close window
|
||||||
if newActiveTabId == "" && recursive {
|
if newActiveTabId == "" && recursive {
|
||||||
|
log.Printf("no tabs remaining in workspace %s, closing window\n", workspaceId)
|
||||||
windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, workspaceId)
|
windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, workspaceId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return newActiveTabId, fmt.Errorf("unable to find window for workspace id %v: %w", workspaceId, err)
|
return newActiveTabId, fmt.Errorf("unable to find window for workspace id %v: %w", workspaceId, err)
|
||||||
@ -159,7 +178,7 @@ func DeleteTab(ctx context.Context, workspaceId string, tabId string, recursive
|
|||||||
}
|
}
|
||||||
|
|
||||||
func SetActiveTab(ctx context.Context, workspaceId string, tabId string) error {
|
func SetActiveTab(ctx context.Context, workspaceId string, tabId string) error {
|
||||||
if tabId != "" {
|
if tabId != "" && workspaceId != "" {
|
||||||
workspace, err := GetWorkspace(ctx, workspaceId)
|
workspace, err := GetWorkspace(ctx, workspaceId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("workspace %s not found: %w", workspaceId, err)
|
return fmt.Errorf("workspace %s not found: %w", workspaceId, err)
|
||||||
@ -174,6 +193,30 @@ func SetActiveTab(ctx context.Context, workspaceId string, tabId string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ChangeTabPinning(ctx context.Context, workspaceId string, tabId string, pinned bool) error {
|
||||||
|
if tabId != "" && workspaceId != "" {
|
||||||
|
workspace, err := GetWorkspace(ctx, workspaceId)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("workspace %s not found: %w", workspaceId, err)
|
||||||
|
}
|
||||||
|
if pinned && utilfn.FindStringInSlice(workspace.PinnedTabIds, tabId) == -1 {
|
||||||
|
if utilfn.FindStringInSlice(workspace.TabIds, tabId) == -1 {
|
||||||
|
return fmt.Errorf("tab %s not found in workspace %s", tabId, workspaceId)
|
||||||
|
}
|
||||||
|
workspace.TabIds = utilfn.RemoveElemFromSlice(workspace.TabIds, tabId)
|
||||||
|
workspace.PinnedTabIds = append(workspace.PinnedTabIds, tabId)
|
||||||
|
} else if !pinned && utilfn.FindStringInSlice(workspace.PinnedTabIds, tabId) != -1 {
|
||||||
|
if utilfn.FindStringInSlice(workspace.PinnedTabIds, tabId) == -1 {
|
||||||
|
return fmt.Errorf("tab %s not found in workspace %s", tabId, workspaceId)
|
||||||
|
}
|
||||||
|
workspace.PinnedTabIds = utilfn.RemoveElemFromSlice(workspace.PinnedTabIds, tabId)
|
||||||
|
workspace.TabIds = append([]string{tabId}, workspace.TabIds...)
|
||||||
|
}
|
||||||
|
wstore.DBUpdate(ctx, workspace)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func SendActiveTabUpdate(ctx context.Context, workspaceId string, newActiveTabId string) {
|
func SendActiveTabUpdate(ctx context.Context, workspaceId string, newActiveTabId string) {
|
||||||
eventbus.SendEventToElectron(eventbus.WSEventType{
|
eventbus.SendEventToElectron(eventbus.WSEventType{
|
||||||
EventType: eventbus.WSEvent_ElectronUpdateActiveTab,
|
EventType: eventbus.WSEvent_ElectronUpdateActiveTab,
|
||||||
@ -181,12 +224,13 @@ func SendActiveTabUpdate(ctx context.Context, workspaceId string, newActiveTabId
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateWorkspaceTabIds(ctx context.Context, workspaceId string, tabIds []string) error {
|
func UpdateWorkspaceTabIds(ctx context.Context, workspaceId string, tabIds []string, pinnedTabIds []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)
|
||||||
}
|
}
|
||||||
ws.TabIds = tabIds
|
ws.TabIds = tabIds
|
||||||
|
ws.PinnedTabIds = pinnedTabIds
|
||||||
wstore.DBUpdate(ctx, ws)
|
wstore.DBUpdate(ctx, ws)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -350,12 +350,28 @@ func DBFindTabForBlockId(ctx context.Context, blockId string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DBFindWorkspaceForTabId(ctx context.Context, tabId string) (string, error) {
|
func DBFindWorkspaceForTabId(ctx context.Context, tabId string) (string, error) {
|
||||||
|
log.Printf("DBFindWorkspaceForTabId tabId: %s\n", tabId)
|
||||||
return WithTxRtn(ctx, func(tx *TxWrap) (string, error) {
|
return WithTxRtn(ctx, func(tx *TxWrap) (string, error) {
|
||||||
query := `
|
query := `
|
||||||
|
WITH variable(value) AS (
|
||||||
|
SELECT ?
|
||||||
|
)
|
||||||
SELECT w.oid
|
SELECT w.oid
|
||||||
FROM db_workspace w, json_each(data->'tabids') je
|
FROM db_workspace w, variable
|
||||||
WHERE je.value = ?`
|
WHERE EXISTS (
|
||||||
return tx.GetString(query, tabId), nil
|
SELECT 1
|
||||||
|
FROM json_each(w.data, '$.tabids') AS je
|
||||||
|
WHERE je.value = variable.value
|
||||||
|
)
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM json_each(w.data, '$.pinnedtabids') AS je
|
||||||
|
WHERE je.value = variable.value
|
||||||
|
);
|
||||||
|
`
|
||||||
|
wsId := tx.GetString(query, tabId)
|
||||||
|
log.Printf("DBFindWorkspaceForTabId wsId: %s\n", wsId)
|
||||||
|
return wsId, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user