From b6c85e38f6a7c96f802cef0205ad90853d149c4c Mon Sep 17 00:00:00 2001 From: Red J Adaya Date: Tue, 18 Jun 2024 12:50:33 +0800 Subject: [PATCH] DnD tabs (#44) --- frontend/app/element/button.less | 48 ++- frontend/app/element/button.tsx | 67 ++-- frontend/app/store/services.ts | 5 + frontend/app/tab/tab.less | 124 ++++++-- frontend/app/tab/tab.tsx | 85 ++--- frontend/app/tab/tabbar.less | 32 ++ frontend/app/tab/tabbar.tsx | 348 +++++++++++++++++++++ frontend/app/tab/tabcontent.less | 33 ++ frontend/app/tab/tabcontent.tsx | 66 ++++ frontend/app/theme.less | 2 + frontend/app/workspace/workspace.less | 53 ---- frontend/app/workspace/workspace.tsx | 54 +--- frontend/types/gotypes.d.ts | 22 +- pkg/service/objectservice/objectservice.go | 17 + pkg/wstore/wstore.go | 12 + 15 files changed, 705 insertions(+), 263 deletions(-) create mode 100644 frontend/app/tab/tabbar.less create mode 100644 frontend/app/tab/tabbar.tsx create mode 100644 frontend/app/tab/tabcontent.less create mode 100644 frontend/app/tab/tabcontent.tsx diff --git a/frontend/app/element/button.less b/frontend/app/element/button.less index 11dcaff6b..1f375f716 100644 --- a/frontend/app/element/button.less +++ b/frontend/app/element/button.less @@ -1,8 +1,8 @@ -// Copyright 2024, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 +/* Copyright 2024, Command Line Inc. */ +/* SPDX-License-Identifier: Apache-2.0 */ -.wave-button { - background: none; +.button { + background: var(--accent-color); border: none; cursor: pointer; outline: inherit; @@ -18,14 +18,16 @@ -webkit-user-select: none; color: var(--main-text-color); - background: var(--accent-color); + i { fill: var(--main-text-color); } - &.primary { + &.primary, + &.secondary { color: var(--main-text-color); background: var(--accent-color); + i { fill: var(--main-text-color); } @@ -35,7 +37,8 @@ background: var(--error-color); } - &.primary.outlined { + &.primary.outlined, + &.primary.greyoutlined { background: none; border: 1px solid var(--accent-color); @@ -45,56 +48,50 @@ } &.primary.greyoutlined { - background: none; - border: 1px solid var(--secondary-text-color); + border-color: var(--secondary-text-color); i { fill: var(--secondary-text-color); } } + &.primary.outlined.danger { + border-color: var(--error-color); + + i { + fill: var(--error-color); + } + } + &.primary.outlined, &.primary.greyoutlined { &.hover-danger:hover { color: var(--main-text-color); - border: 1px solid var(--error-color); + border-color: var(--error-color); background: var(--error-color); } } - &.primary.outlined.danger { - background: none; - border: 1px solid var(--error-color); - - i { - fill: var(--error-color); - } - } - &.greytext { color: var(--secondary-text-color); } &.primary.ghost { background: none; + i { fill: var(--accent-color); } } &.primary.ghost.danger { - background: none; i { fill: var(--app-error-color); } } &.secondary { - color: var(--main-text-color); background: var(--highlight-bg-color); - i { - fill: var(--main-text-color); - } } &.secondary.outlined { @@ -103,8 +100,7 @@ } &.secondary.outlined.danger { - background: none; - border: 1px solid var(--error-color); + border-color: var(--error-color); } &.secondary.ghost { diff --git a/frontend/app/element/button.tsx b/frontend/app/element/button.tsx index 7b65f371b..9153c6fe7 100644 --- a/frontend/app/element/button.tsx +++ b/frontend/app/element/button.tsx @@ -1,55 +1,28 @@ -// Copyright 2024, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { clsx } from "clsx"; -import * as React from "react"; - +import clsx from "clsx"; +import React from "react"; import "./button.less"; -interface ButtonProps { - children: React.ReactNode; - onClick?: (e: React.MouseEvent) => void; - disabled?: boolean; - leftIcon?: React.ReactNode; - rightIcon?: React.ReactNode; - style?: React.CSSProperties; - autoFocus?: boolean; +interface ButtonProps extends React.ButtonHTMLAttributes { className?: string; - termInline?: boolean; - title?: string; } -class Button extends React.Component { - static defaultProps = { - style: {}, - className: "primary", - }; +const Button: React.FC = ({ className = "primary", children, disabled, ...props }) => { + const hasIcon = React.Children.toArray(children).some( + (child) => React.isValidElement(child) && (child as React.ReactElement).type === "svg" + ); - handleClick(e) { - if (this.props.onClick && !this.props.disabled) { - this.props.onClick(e); - } - } - - render() { - const { leftIcon, rightIcon, children, disabled, style, autoFocus, termInline, className, title } = this.props; - - return ( - - ); - } -} + return ( + + ); +}; export { Button }; -export type { ButtonProps }; diff --git a/frontend/app/store/services.ts b/frontend/app/store/services.ts index a16d23ca5..a109bf4b7 100644 --- a/frontend/app/store/services.ts +++ b/frontend/app/store/services.ts @@ -94,6 +94,11 @@ class ObjectServiceType { UpdateObjectMeta(oref: string, meta: MetaType): Promise { return WOS.callBackendService("object", "UpdateObjectMeta", Array.from(arguments)) } + + // @returns object updates + UpdateWorkspaceTabIds(workspaceId: string, tabIds: string[]): Promise { + return WOS.callBackendService("object", "UpdateWorkspaceTabIds", Array.from(arguments)) + } } export const ObjectService = new ObjectServiceType() diff --git a/frontend/app/tab/tab.less b/frontend/app/tab/tab.less index 6efdb348a..77a7e24b6 100644 --- a/frontend/app/tab/tab.less +++ b/frontend/app/tab/tab.less @@ -1,33 +1,105 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -.tabcontent { - display: flex; - flex-direction: row; - flex-grow: 1; - min-height: 0; - width: 100%; - align-items: center; - justify-content: center; - overflow: hidden; +.tab { + cursor: pointer; + width: 130px; + height: 100%; + position: absolute; + box-sizing: border-box; + cursor: pointer; + font-weight: bold; + color: var(--secondary-text-color); + white-space: nowrap; + border-top: 2px solid transparent; + background-color: rgba(0, 8, 3, 0); - .block-container { + &.animate { + transition: + transform 0.3s ease, + background-color 0.3s ease-in-out; + } + + &.active { + border-top: 2px solid var(--tab-green); + background-color: var(--tab-green); + } + + .name { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + -webkit-user-select: none; + z-index: 3; + } + + .vertical-line { + display: inline; + width: 1px; + height: 50%; + position: absolute; + right: -1px; + top: 50%; + z-index: 1; + transform: translateY(-50%); + background-color: var(--border-color); + } + + .close { + visibility: hidden; + position: absolute; + width: 20px; + height: 20px; + padding: 0; display: flex; - flex-direction: row; - flex: 1 0 0; - height: 100%; - overflow: hidden; - border: 1px solid var(--border-color); - border-radius: 4px; + align-items: center; + justify-content: center; + cursor: pointer; + top: 50%; + z-index: 3; + transform: translateY(-50%); + right: 5px; + + &:hover { + border: 1px solid var(--border-color); + border-radius: 2px; + } + + i { + color: var(--secondary-text-color); + } + } + + &:hover .close { + visibility: visible; + } + + &.active { + .vertical-line { + visibility: hidden; + } + } + + &.active { + .mask { + position: absolute; + height: 100%; + width: 100%; + top: 0; + left: 0; + z-index: 2; + background-image: linear-gradient( + to top, + rgba(0, 0, 0, 0.9) 20%, + rgba(0, 0, 0, 0.8) 60%, + rgba(0, 0, 0, 0.7) 100% + ); + pointer-events: none; /* Prevents the background from capturing mouse events */ + } + } + + &.isDragging:not(.active) { + background-color: rgba(0, 8, 3, 1); } } - -.drag-preview { - display: block; - width: 100px; - height: 20px; - border-radius: 2px; - background-color: aquamarine; - color: black; - text-align: center; -} diff --git a/frontend/app/tab/tab.tsx b/frontend/app/tab/tab.tsx index bc09c70d1..9f1ff225e 100644 --- a/frontend/app/tab/tab.tsx +++ b/frontend/app/tab/tab.tsx @@ -1,66 +1,45 @@ -// Copyright 2023, Command Line Inc. +// Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { Block, BlockHeader } from "@/app/block/block"; -import * as services from "@/store/services"; +import { Button } from "@/element/button"; import * as WOS from "@/store/wos"; +import { clsx } from "clsx"; +import React from "react"; -import { CenteredDiv, CenteredLoadingDiv } from "@/element/quickelems"; -import { TileLayout } from "@/faraday/index"; -import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom"; -import { useAtomValue } from "jotai"; -import { useCallback, useMemo } from "react"; import "./tab.less"; -const TabContent = ({ tabId }: { tabId: string }) => { - const oref = useMemo(() => WOS.makeORef("tab", tabId), [tabId]); - const loadingAtom = useMemo(() => WOS.getWaveObjectLoadingAtom(oref), [oref]); - const tabLoading = useAtomValue(loadingAtom); - const tabAtom = useMemo(() => WOS.getWaveObjectAtom(oref), [oref]); - const layoutStateAtom = useMemo(() => getLayoutStateAtomForTab(tabId, tabAtom), [tabAtom, tabId]); - const tabData = useAtomValue(tabAtom); +interface TabProps { + id: string; + active: boolean; + isBeforeActive: boolean; + isDragging: boolean; + onSelect: () => void; + onClose: () => void; + onDragStart: () => void; +} - const renderBlock = useCallback((tabData: TabLayoutData, ready: boolean, onClose: () => void) => { - // console.log("renderBlock", tabData); - if (!tabData.blockId || !ready) { - return null; - } - return ; - }, []); +const Tab = React.forwardRef( + ({ id, active, isBeforeActive, isDragging, onSelect, onClose, onDragStart }, ref) => { + const [tabData, tabLoading] = WOS.useWaveObjectValue(WOS.makeORef("tab", id)); + const name = tabData?.name ?? "..."; - const renderPreview = useCallback((tabData: TabLayoutData) => { - console.log("renderPreview", tabData); - return ; - }, []); - - const onNodeDelete = useCallback((data: TabLayoutData) => { - console.log("onNodeDelete", data); - return services.ObjectService.DeleteBlock(data.blockId); - }, []); - - if (tabLoading) { - return ; - } - - if (!tabData) { return ( -
- Tab Not Found +
+
{name}
+ {!isDragging &&
} + {active &&
} +
); } +); - return ( -
- -
- ); -}; - -export { TabContent }; +export { Tab }; diff --git a/frontend/app/tab/tabbar.less b/frontend/app/tab/tabbar.less new file mode 100644 index 000000000..d6b8c536c --- /dev/null +++ b/frontend/app/tab/tabbar.less @@ -0,0 +1,32 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +.tab-bar-wrapper { + position: relative; + border-bottom: 1px solid var(--border-color); + -webkit-user-select: none; + + .tab-bar { + position: relative; // Needed for absolute positioning of child tabs + min-height: 34px; // Adjust as necessary to fit the height of tabs + width: calc(100vw - 36px); // 36 is the width of add tab button + overflow: hidden; + } + + .add-tab-btn { + width: 36px; + height: 34px; + cursor: pointer; + position: absolute; + top: 50%; + transform: translateY(-50%); // overridden in js + border-radius: 100%; + font-size: 14px; + text-align: center; + height: 32px; + user-select: none; + display: flex; + align-items: center; + justify-content: center; + } +} diff --git a/frontend/app/tab/tabbar.tsx b/frontend/app/tab/tabbar.tsx new file mode 100644 index 000000000..8ead33ec2 --- /dev/null +++ b/frontend/app/tab/tabbar.tsx @@ -0,0 +1,348 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { deleteLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom"; +import { atoms } from "@/store/global"; +import * as services from "@/store/services"; +import { PrimitiveAtom, atom, useAtom, useAtomValue } from "jotai"; +import React, { createRef, useCallback, useEffect, useRef } from "react"; + +import { Tab } from "./tab"; + +import "./tabbar.less"; + +const DEFAULT_TAB_WIDTH = 130; + +// Atoms +const tabIdsAtom = atom([]); +const tabWidthAtom = atom(DEFAULT_TAB_WIDTH); +const dragStartPositionsAtom = atom([]); +const draggingTabAtom = atom(null) as PrimitiveAtom; +const loadingAtom = atom(true); + +interface TabBarProps { + workspace: Workspace; +} + +const TabBar = ({ workspace }: TabBarProps) => { + const [tabIds, setTabIds] = useAtom(tabIdsAtom); + const [tabWidth, setTabWidth] = useAtom(tabWidthAtom); + const [dragStartPositions, setDragStartPositions] = useAtom(dragStartPositionsAtom); + const [draggingTab, setDraggingTab] = useAtom(draggingTabAtom); + const [loading, setLoading] = useAtom(loadingAtom); + + const tabBarRef = useRef(null); + const tabRefs = useRef[]>([]); + const addBtnRef = useRef(null); + const draggingTimeoutId = useRef(null); + const draggingRemovedRef = useRef(false); + const draggingTabDataRef = useRef({ + tabId: "", + ref: { current: null }, + tabStartX: 0, + tabIndex: 0, + dragged: false, + }); + + const windowData = useAtomValue(atoms.waveWindow); + const { activetabid } = windowData; + + let prevDelta: number; + let prevDragDirection: string; + let shrunk: boolean; + + // Update refs when tabIds change + useEffect(() => { + tabRefs.current = tabIds.map((_, index) => tabRefs.current[index] || createRef()); + }, [tabIds]); + + useEffect(() => { + if (workspace) { + // Compare current tabIds with new workspace.tabids + const currentTabIds = new Set(tabIds); + const newTabIds = new Set(workspace.tabids); + + const areEqual = + currentTabIds.size === newTabIds.size && [...currentTabIds].every((id) => newTabIds.has(id)); + + if (!areEqual) { + setTabIds(workspace.tabids); + } + setLoading(false); + } + }, [workspace, tabIds, setTabIds, setLoading]); + + const updateTabPositions = useCallback(() => { + if (tabBarRef.current) { + const newStartPositions: number[] = []; + let cumulativeLeft = 0; // Start from the left edge + + tabRefs.current.forEach((ref) => { + if (ref.current) { + newStartPositions.push(cumulativeLeft); + cumulativeLeft += ref.current.getBoundingClientRect().width; // Add each tab's actual width to the cumulative position + } + }); + + setDragStartPositions(newStartPositions); + } + }, [tabRefs.current, setDragStartPositions]); + + const handleResizeTabs = useCallback(() => { + const tabBar = tabBarRef.current; + if (!tabBar) return; + + const containerWidth = tabBar.getBoundingClientRect().width; + const numberOfTabs = tabIds.length; + const totalDefaultTabWidth = numberOfTabs * DEFAULT_TAB_WIDTH; + let newTabWidth = DEFAULT_TAB_WIDTH; + + if (totalDefaultTabWidth > containerWidth) { + newTabWidth = containerWidth / numberOfTabs; + shrunk = true; + } else { + shrunk = false; + } + + // Apply the calculated width and position to all tabs + tabRefs.current.forEach((ref, index) => { + if (ref.current) { + ref.current.style.width = `${newTabWidth}px`; + ref.current.style.transform = `translateX(${index * newTabWidth}px)`; + } + }); + + // Update the state with the new tab width if it has changed + if (newTabWidth !== tabWidth) { + setTabWidth(newTabWidth); + } + + // Update the position of the Add Tab button if needed + const addButton = addBtnRef.current; + const lastTabRef = tabRefs.current[tabRefs.current.length - 1]; + if (addButton && lastTabRef && lastTabRef.current) { + const lastTabRect = lastTabRef.current.getBoundingClientRect(); + addButton.style.position = "absolute"; + addButton.style.transform = `translateX(${lastTabRect.right}px) translateY(-50%)`; + } + }, [tabIds, tabWidth, updateTabPositions, setTabWidth]); + + useEffect(() => { + window.addEventListener("resize", handleResizeTabs); + return () => { + window.removeEventListener("resize", handleResizeTabs); + }; + }, [handleResizeTabs]); + + useEffect(() => { + if (!loading) { + handleResizeTabs(); + updateTabPositions(); + } + }, [loading, handleResizeTabs, updateTabPositions]); + + // Make sure timeouts are cleared when component is unmounted + useEffect(() => { + return () => { + if (draggingTimeoutId.current) { + clearTimeout(draggingTimeoutId.current); + } + }; + }, []); + + const handleMouseMove = (event: MouseEvent) => { + const { tabId, ref, tabStartX } = draggingTabDataRef.current; + + let tabIndex = draggingTabDataRef.current.tabIndex; + let currentX = event.clientX - ref.current.getBoundingClientRect().width / 2; + + // Check if the tab has moved 5 pixels + if (Math.abs(currentX - tabStartX) >= 5) { + setDraggingTab(tabId); + draggingTabDataRef.current.dragged = true; + } + + // Constrain movement within the container bounds + if (tabBarRef.current) { + const numberOfTabs = tabIds.length; + const totalDefaultTabWidth = numberOfTabs * DEFAULT_TAB_WIDTH; + const containerRect = tabBarRef.current.getBoundingClientRect(); + let containerRectWidth = containerRect.width; + // Set to the total default tab width if there's vacant space + if (totalDefaultTabWidth < containerRectWidth) { + containerRectWidth = totalDefaultTabWidth; + } + + const minLeft = 0; + const maxRight = containerRectWidth - tabWidth; + + // Adjust currentX to stay within bounds + currentX = Math.min(Math.max(currentX, minLeft), maxRight); + } + + ref.current!.style.transform = `translateX(${currentX}px)`; + ref.current!.style.zIndex = "100"; + + let dragDirection; + if (currentX - prevDelta > 0) { + dragDirection = "+"; + } else if (currentX - prevDelta === 0) { + dragDirection = prevDragDirection; + } else { + dragDirection = "-"; + } + prevDelta = currentX; + prevDragDirection = dragDirection; + + let newTabIndex = tabIndex; + + if (dragDirection === "+") { + // Dragging to the right + for (let i = tabIndex + 1; i < tabIds.length; i++) { + const otherTabStart = dragStartPositions[i]; + if (currentX + tabWidth > otherTabStart + tabWidth / 2) { + newTabIndex = i; + } + } + } else { + // Dragging to the left + for (let i = tabIndex - 1; i >= 0; i--) { + const otherTabEnd = dragStartPositions[i] + tabWidth; + if (currentX < otherTabEnd - tabWidth / 2) { + newTabIndex = i; + } + } + } + + if (newTabIndex !== tabIndex) { + // Remove the dragged tab if not already done + if (!draggingRemovedRef.current) { + tabIds.splice(tabIndex, 1); + draggingRemovedRef.current = true; + } + + // Find current index of the dragged tab in tempTabs + const currentIndexOfDraggingTab = tabIds.indexOf(tabId); + + // Move the dragged tab to its new position + if (currentIndexOfDraggingTab !== -1) { + tabIds.splice(currentIndexOfDraggingTab, 1); + } + tabIds.splice(newTabIndex, 0, tabId); + + // Update visual positions of the tabs + tabIds.forEach((localTabId, index) => { + const ref = tabRefs.current.find((ref) => ref.current.dataset.tabId === localTabId); + if (ref.current && localTabId !== tabId) { + ref.current.style.transform = `translateX(${index * tabWidth}px)`; + ref.current.classList.add("animate"); + } + }); + + tabIndex = newTabIndex; + draggingTabDataRef.current.tabIndex = newTabIndex; + } + }; + + const handleMouseUp = (event: MouseEvent) => { + const { tabIndex, dragged } = draggingTabDataRef.current; + + // Update the final position of the dragged tab + const draggingTab = tabIds[tabIndex]; + const finalLeftPosition = tabIndex * tabWidth; + const ref = tabRefs.current.find((ref) => ref.current.dataset.tabId === draggingTab); + if (ref.current) { + ref.current.classList.add("animate"); + ref.current.style.transform = `translateX(${finalLeftPosition}px)`; + } + + if (dragged) { + draggingTimeoutId.current = setTimeout(() => { + // 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 + services.ObjectService.UpdateWorkspaceTabIds(workspace.oid, tabIds); + }, 300); + } + + document.removeEventListener("mouseup", handleMouseUp); + document.removeEventListener("mousemove", handleMouseMove); + draggingRemovedRef.current = false; + }; + + const handleDragStart = useCallback( + (name: string, ref: React.RefObject) => { + const tabIndex = tabIds.indexOf(name); + const tabStartX = dragStartPositions[tabIndex]; // Starting X position of the tab + + if (ref.current) { + draggingTabDataRef.current = { + tabId: ref.current.dataset.tabId, + ref, + tabStartX, + tabIndex, + dragged: false, + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + + if (draggingTimeoutId.current) { + clearTimeout(draggingTimeoutId.current); + } + } + }, + [tabIds, dragStartPositions, tabWidth] + ); + + const handleSelectTab = (tabId: string) => { + if (!draggingTabDataRef.current.dragged) { + services.ObjectService.SetActiveTab(tabId); + } + }; + + const handleAddTab = () => { + const newTabName = `T${tabIds.length + 1}`; + setTabIds([...tabIds, newTabName]); + services.ObjectService.AddTabToWorkspace(newTabName, true); + }; + + const handleCloseTab = (tabId: string) => { + services.ObjectService.CloseTab(tabId); + deleteLayoutStateAtomForTab(tabId); + }; + + const isBeforeActive = (tabId: string) => { + return tabIds.indexOf(tabId) === tabIds.indexOf(activetabid) - 1; + }; + + return ( +
+
+ {tabIds.map((tabId, index) => ( + handleSelectTab(tabId)} + active={activetabid === tabId} + onDragStart={() => handleDragStart(tabId, tabRefs.current[index])} + onClose={() => handleCloseTab(tabId)} + isBeforeActive={isBeforeActive(tabId)} + isDragging={draggingTab === tabId} + /> + ))} +
+
+ +
+
+ ); +}; + +export { TabBar }; diff --git a/frontend/app/tab/tabcontent.less b/frontend/app/tab/tabcontent.less new file mode 100644 index 000000000..6efdb348a --- /dev/null +++ b/frontend/app/tab/tabcontent.less @@ -0,0 +1,33 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +.tabcontent { + display: flex; + flex-direction: row; + flex-grow: 1; + min-height: 0; + width: 100%; + align-items: center; + justify-content: center; + overflow: hidden; + + .block-container { + display: flex; + flex-direction: row; + flex: 1 0 0; + height: 100%; + overflow: hidden; + border: 1px solid var(--border-color); + border-radius: 4px; + } +} + +.drag-preview { + display: block; + width: 100px; + height: 20px; + border-radius: 2px; + background-color: aquamarine; + color: black; + text-align: center; +} diff --git a/frontend/app/tab/tabcontent.tsx b/frontend/app/tab/tabcontent.tsx new file mode 100644 index 000000000..618a3cc43 --- /dev/null +++ b/frontend/app/tab/tabcontent.tsx @@ -0,0 +1,66 @@ +// Copyright 2023, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { Block, BlockHeader } from "@/app/block/block"; +import * as services from "@/store/services"; +import * as WOS from "@/store/wos"; + +import { CenteredDiv, CenteredLoadingDiv } from "@/element/quickelems"; +import { TileLayout } from "@/faraday/index"; +import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom"; +import { useAtomValue } from "jotai"; +import { useCallback, useMemo } from "react"; +import "./tabcontent.less"; + +const TabContent = ({ tabId }: { tabId: string }) => { + const oref = useMemo(() => WOS.makeORef("tab", tabId), [tabId]); + const loadingAtom = useMemo(() => WOS.getWaveObjectLoadingAtom(oref), [oref]); + const tabLoading = useAtomValue(loadingAtom); + const tabAtom = useMemo(() => WOS.getWaveObjectAtom(oref), [oref]); + const layoutStateAtom = useMemo(() => getLayoutStateAtomForTab(tabId, tabAtom), [tabAtom, tabId]); + const tabData = useAtomValue(tabAtom); + + const renderBlock = useCallback((tabData: TabLayoutData, ready: boolean, onClose: () => void) => { + // console.log("renderBlock", tabData); + if (!tabData.blockId || !ready) { + return null; + } + return ; + }, []); + + const renderPreview = useCallback((tabData: TabLayoutData) => { + console.log("renderPreview", tabData); + return ; + }, []); + + const onNodeDelete = useCallback((data: TabLayoutData) => { + console.log("onNodeDelete", data); + return services.ObjectService.DeleteBlock(data.blockId); + }, []); + + if (tabLoading) { + return ; + } + + if (!tabData) { + return ( +
+ Tab Not Found +
+ ); + } + + return ( +
+ +
+ ); +}; + +export { TabContent }; diff --git a/frontend/app/theme.less b/frontend/app/theme.less index 5bf864211..9d54bb8c1 100644 --- a/frontend/app/theme.less +++ b/frontend/app/theme.less @@ -23,6 +23,8 @@ --scrollbar-thumb-color: rgba(255, 255, 255, 0.3); --scrollbar-thumb-hover-color: rgba(255, 255, 255, 0.5); + --tab-green: rgb(88, 193, 66); + /* z-index values */ --zindex-header-hover: 100; } diff --git a/frontend/app/workspace/workspace.less b/frontend/app/workspace/workspace.less index 609e619bb..540bec624 100644 --- a/frontend/app/workspace/workspace.less +++ b/frontend/app/workspace/workspace.less @@ -39,56 +39,3 @@ } } } - -.tab-bar { - display: flex; - flex-direction: row; - height: 32px; - border-bottom: 1px solid var(--border-color); - flex-shrink: 0; - - .tab { - display: flex; - justify-content: center; - align-items: center; - width: 100px; - height: 100%; - font-weight: bold; - border-right: 1px solid var(--border-color); - user-select: none; - -webkit-user-select: none; - cursor: pointer; - position: relative; - - &.active { - background-color: var(--highlight-bg-color); - } - - &.active:hover .tab-close { - display: block; - } - - .tab-close { - position: absolute; - display: none; - padding: 5px; - right: 2px; - top: 5px; - cursor: pointer; - } - } - - .tab-add { - display: flex; - justify-content: center; - align-items: center; - width: 40px; - height: 100%; - cursor: pointer; - border-left: 1px solid transparent; - &:hover { - border-left: 1px solid white; - background-color: var(--highlight-bg-color); - } - } -} diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index 80a35342c..de84c492d 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -1,66 +1,19 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { TabContent } from "@/app/tab/tab"; +import { TabBar } from "@/app/tab/tabbar"; +import { TabContent } from "@/app/tab/tabcontent"; import { atoms } from "@/store/global"; import * as services from "@/store/services"; import * as WOS from "@/store/wos"; -import { clsx } from "clsx"; import * as jotai from "jotai"; import { CenteredDiv } from "../element/quickelems"; import { LayoutTreeActionType, LayoutTreeInsertNodeAction, newLayoutNode } from "@/faraday/index"; -import { - deleteLayoutStateAtomForTab, - getLayoutStateAtomForTab, - useLayoutTreeStateReducerAtom, -} from "@/faraday/lib/layoutAtom"; +import { getLayoutStateAtomForTab, useLayoutTreeStateReducerAtom } from "@/faraday/lib/layoutAtom"; import { useMemo } from "react"; import "./workspace.less"; -function Tab({ tabId }: { tabId: string }) { - const windowData = jotai.useAtomValue(atoms.waveWindow); - const [tabData, tabLoading] = WOS.useWaveObjectValue(WOS.makeORef("tab", tabId)); - function setActiveTab() { - services.ObjectService.SetActiveTab(tabId); - } - function handleCloseTab() { - services.ObjectService.CloseTab(tabId); - deleteLayoutStateAtomForTab(tabId); - } - return ( -
setActiveTab()} - > -
handleCloseTab()}> -
- -
-
- {tabData?.name ?? "..."} -
- ); -} - -function TabBar({ workspace }: { workspace: Workspace }) { - function handleAddTab() { - const newTabName = `Tab-${workspace.tabids.length + 1}`; - services.ObjectService.AddTabToWorkspace(newTabName, true); - } - const tabIds = workspace?.tabids ?? []; - return ( -
- {tabIds.map((tabid, idx) => { - return ; - })} -
handleAddTab()}> - -
-
- ); -} - function Widgets() { const windowData = jotai.useAtomValue(atoms.waveWindow); const activeTabAtom = useMemo(() => { @@ -149,6 +102,7 @@ function WorkspaceElem() { const windowData = jotai.useAtomValue(atoms.waveWindow); const activeTabId = windowData?.activetabid; const ws = jotai.useAtomValue(atoms.workspace); + console.log("ws", ws); return (
diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index b0011693f..926f130f4 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -4,7 +4,6 @@ // generated by cmd/generate/main-generate.go declare global { - // wstore.Block type Block = WaveObj & { blockdef: BlockDef; @@ -30,13 +29,21 @@ declare global { type BlockCommand = { command: string; - } & ( BlockSetMetaCommand | BlockGetMetaCommand | BlockMessageCommand | BlockAppendFileCommand | BlockAppendIJsonCommand | BlockInputCommand | BlockSetViewCommand ); + } & ( + | BlockSetMetaCommand + | BlockGetMetaCommand + | BlockMessageCommand + | BlockAppendFileCommand + | BlockAppendIJsonCommand + | BlockInputCommand + | BlockSetViewCommand + ); // wstore.BlockDef type BlockDef = { controller?: string; view?: string; - files?: {[key: string]: FileDef}; + files?: { [key: string]: FileDef }; meta?: MetaType; }; @@ -111,7 +118,7 @@ declare global { meta?: MetaType; }; - type MetaType = {[key: string]: any} + type MetaType = { [key: string]: any }; // tsgenmeta.MethodMeta type MethodMeta = { @@ -167,7 +174,7 @@ declare global { type WSCommandType = { wscommand: string; - } & ( SetBlockTermSizeWSCommand ); + } & SetBlockTermSizeWSCommand; // eventbus.WSEventType type WSEventType = { @@ -226,7 +233,7 @@ declare global { workspaceid: string; activetabid: string; activeblockid?: string; - activeblockmap: {[key: string]: string}; + activeblockmap: { [key: string]: string }; pos: Point; winsize: WinSize; lastfocusts: number; @@ -239,7 +246,6 @@ declare global { tabids: string[]; meta: MetaType; }; - } -export {} +export {}; diff --git a/pkg/service/objectservice/objectservice.go b/pkg/service/objectservice/objectservice.go index 7e2d8599c..b05859f63 100644 --- a/pkg/service/objectservice/objectservice.go +++ b/pkg/service/objectservice/objectservice.go @@ -99,6 +99,23 @@ func (svc *ObjectService) AddTabToWorkspace(uiContext wstore.UIContext, tabName return tab.OID, wstore.ContextGetUpdatesRtn(ctx), nil } +func (svc *ObjectService) UpdateWorkspaceTabIds_Meta() tsgenmeta.MethodMeta { + return tsgenmeta.MethodMeta{ + ArgNames: []string{"uiContext", "workspaceId", "tabIds"}, + } +} + +func (svc *ObjectService) UpdateWorkspaceTabIds(uiContext wstore.UIContext, workspaceId string, tabIds []string) (wstore.UpdatesRtnType, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + ctx = wstore.ContextWithUpdates(ctx) + err := wstore.UpdateWorkspaceTabIds(ctx, workspaceId, tabIds) + if err != nil { + return nil, fmt.Errorf("error updating workspace tab ids: %w", err) + } + return wstore.ContextGetUpdatesRtn(ctx), nil +} + func (svc *ObjectService) SetActiveTab_Meta() tsgenmeta.MethodMeta { return tsgenmeta.MethodMeta{ ArgNames: []string{"uiContext", "tabId"}, diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index 7815ca13e..017f06bb1 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -181,6 +181,18 @@ func CreateWorkspace(ctx context.Context) (*Workspace, error) { return ws, nil } +func UpdateWorkspaceTabIds(ctx context.Context, workspaceId string, tabIds []string) error { + return WithTx(ctx, func(tx *TxWrap) error { + ws, _ := DBGet[*Workspace](tx.Context(), workspaceId) + if ws == nil { + return fmt.Errorf("workspace not found: %q", workspaceId) + } + ws.TabIds = tabIds + DBUpdate(tx.Context(), ws) + return nil + }) +} + func SetActiveTab(ctx context.Context, windowId string, tabId string) error { return WithTx(ctx, func(tx *TxWrap) error { window, _ := DBGet[*Window](tx.Context(), windowId)