diff --git a/packages/bruno-app/src/components/RequestTabs/ExampleTab/index.js b/packages/bruno-app/src/components/RequestTabs/ExampleTab/index.js index 54af20466..7b9490f93 100644 --- a/packages/bruno-app/src/components/RequestTabs/ExampleTab/index.js +++ b/packages/bruno-app/src/components/RequestTabs/ExampleTab/index.js @@ -8,8 +8,7 @@ import ExampleIcon from 'components/Icons/ExampleIcon'; import ConfirmRequestClose from '../RequestTab/ConfirmRequestClose'; import RequestTabNotFound from '../RequestTab/RequestTabNotFound'; import StyledWrapper from '../RequestTab/StyledWrapper'; -import CloseTabIcon from '../RequestTab/CloseTabIcon'; -import DraftTabIcon from '../RequestTab/DraftTabIcon'; +import GradientCloseButton from '../RequestTab/GradientCloseButton'; const ExampleTab = ({ tab, collection }) => { const dispatch = useDispatch(); @@ -59,7 +58,7 @@ const ExampleTab = ({ tab, collection }) => { if (!item || !example) { return ( { if (e.button === 1) { e.preventDefault(); @@ -75,7 +74,7 @@ const ExampleTab = ({ tab, collection }) => { } return ( - + {showConfirmClose && ( { } }} > - + {example.name} -
{ if (!hasChanges) { return handleCloseClick(e); @@ -132,13 +131,7 @@ const ExampleTab = ({ tab, collection }) => { e.preventDefault(); setShowConfirmClose(true); }} - > - {!hasChanges ? ( - - ) : ( - - )} -
+ />
); }; diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/GradientCloseButton/StyledWrapper.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/GradientCloseButton/StyledWrapper.js new file mode 100644 index 000000000..3803db003 --- /dev/null +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/GradientCloseButton/StyledWrapper.js @@ -0,0 +1,106 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div.attrs((props) => ({ + style: { + '--gradient-color': props.theme.requestTabs.bg, + '--gradient-color-active': props.theme.bg + } +}))` + display: flex; + align-items: center; + justify-content: flex-end; + position: absolute; + width: 44px; + height: 100%; + right: 0; + top: 0; + padding-right: 4px; + z-index: 3; + + background-image: linear-gradient( + 90deg, + transparent 0%, + var(--gradient-color) 40% + ); + + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease; + + li.active & { + background-image: linear-gradient( + 90deg, + transparent 0%, + var(--gradient-color-active) 40% + ); + } + + li:hover &, + &.has-changes { + opacity: 1; + pointer-events: auto; + } + + .close-icon-container { + display: flex; + justify-content: center; + align-items: center; + width: 22px; + height: 22px; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.12s ease; + + &:hover { + background-color: ${(props) => props.theme.requestTabs.icon.hoverBg}; + + .close-icon { + color: ${(props) => props.theme.requestTabs.icon.hoverColor}; + } + } + } + + .close-icon { + color: ${(props) => props.theme.requestTabs.icon.color}; + width: 12px; + height: 12px; + transition: color 0.12s ease; + } + + .has-changes-icon { + width: 10px; + height: 10px; + } + + .draft-icon-wrapper { + display: none; + } + + .close-icon-wrapper { + display: flex; + align-items: center; + justify-content: center; + } + + &.has-changes:not(li:hover &) { + .draft-icon-wrapper { + display: flex; + align-items: center; + justify-content: center; + } + .close-icon-wrapper { + display: none; + } + } + + li:hover &.has-changes { + .draft-icon-wrapper { + display: none; + } + .close-icon-wrapper { + display: flex; + } + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/GradientCloseButton/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/GradientCloseButton/index.js new file mode 100644 index 000000000..e34e3f5d0 --- /dev/null +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/GradientCloseButton/index.js @@ -0,0 +1,21 @@ +import React from 'react'; +import CloseTabIcon from '../CloseTabIcon'; +import DraftTabIcon from '../DraftTabIcon'; +import StyledWrapper from './StyledWrapper'; + +const GradientCloseButton = ({ onClick, hasChanges = false }) => { + return ( + +
+ + + + + + +
+
+ ); +}; + +export default GradientCloseButton; diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js index df5bda2a0..0ea63e4fb 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js @@ -1,6 +1,5 @@ import React from 'react'; -import CloseTabIcon from './CloseTabIcon'; -import DraftTabIcon from './DraftTabIcon'; +import GradientCloseButton from './GradientCloseButton'; import { IconVariable, IconSettings, IconRun, IconFolder, IconShieldLock } from '@tabler/icons'; const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick, hasDraft }) => { @@ -8,49 +7,49 @@ const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick, hasDra switch (type) { case 'collection-settings': { return ( -
- - Collection -
+ <> + + Collection + ); } case 'collection-overview': { return ( <> - - Collection + + Overview ); } case 'security-settings': { return ( <> - - Security + + Security ); } case 'folder-settings': { return ( -
- - {tabName || 'Folder'} -
+ <> + + {tabName || 'Folder'} + ); } case 'variables': { return ( <> - - Variables + + Variables ); } case 'collection-runner': { return ( <> - - Runner + + Runner ); } @@ -59,10 +58,13 @@ const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick, hasDra return ( <> -
{getTabInfo(type, tabName)}
-
handleCloseClick(e)}> - {hasDraft ? : } +
+ {getTabInfo(type, tabName)}
+ handleCloseClick(e)} /> ); }; diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/StyledWrapper.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/StyledWrapper.js index 16f67cecf..2c4934dad 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/StyledWrapper.js @@ -1,43 +1,30 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` + position: relative; + width: 100%; + height: 100%; + .tab-label { overflow: hidden; + align-items: center; + position: relative; + flex: 1; + min-width: 0; + } + + .tab-method { + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.02em; + flex-shrink: 0; } .tab-name { + position: relative; overflow: hidden; - text-overflow: ellipsis; white-space: nowrap; - } - - .close-icon-container { - min-height: 20px; - min-width: 24px; - margin-left: 4px; - border-radius: 3px; - - .close-icon { - display: none; - color: ${(props) => props.theme.requestTabs.icon.color}; - width: 8px; - padding-bottom: 6px; - padding-top: 6px; - } - - &:hover, - &:hover .close-icon { - color: ${(props) => props.theme.requestTabs.icon.hoverColor}; - background-color: ${(props) => props.theme.requestTabs.icon.hoverBg}; - } - - .has-changes-icon { - height: 24px; - } - - .tab-method { - font-size: ${(props) => props.theme.font.size.sm}; - } + font-size: 0.8125rem; } `; diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js index f0820badc..0861d873e 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js @@ -1,4 +1,4 @@ -import React, { useCallback, useState, useRef, Fragment, useMemo } from 'react'; +import React, { useCallback, useState, useRef, Fragment, useMemo, useEffect } from 'react'; import get from 'lodash/get'; import { closeTabs, makeTabPermanent } from 'providers/ReduxStore/slices/tabs'; import { saveRequest, saveCollectionRoot, saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions'; @@ -17,16 +17,17 @@ import StyledWrapper from './StyledWrapper'; import Dropdown from 'components/Dropdown'; import CloneCollectionItem from 'components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index'; import NewRequest from 'components/Sidebar/NewRequest/index'; -import CloseTabIcon from './CloseTabIcon'; -import DraftTabIcon from './DraftTabIcon'; +import GradientCloseButton from './GradientCloseButton'; import { flattenItems } from 'utils/collections/index'; import { closeWsConnection } from 'utils/network/index'; import ExampleTab from '../ExampleTab'; -const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid }) => { +const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid, hasOverflow, setHasOverflow }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const theme = storedTheme === 'dark' ? darkTheme : lightTheme; + const tabNameRef = useRef(null); + const lastOverflowStateRef = useRef(null); const [showConfirmClose, setShowConfirmClose] = useState(false); const [showConfirmCollectionClose, setShowConfirmCollectionClose] = useState(false); const [showConfirmFolderClose, setShowConfirmFolderClose] = useState(false); @@ -36,6 +37,48 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi const item = findItemInCollection(collection, tab.uid); + const method = useMemo(() => { + if (!item) return; + switch (item.type) { + case 'grpc-request': + return 'gRPC'; + case 'ws-request': + return 'WS'; + case 'graphql-request': + return 'GQL'; + default: + return item.draft ? get(item, 'draft.request.method') : get(item, 'request.method'); + } + }, [item]); + + useEffect(() => { + if (!item || !tabNameRef.current || !setHasOverflow) return; + + const checkOverflow = () => { + if (tabNameRef.current && setHasOverflow) { + const hasOverflow = tabNameRef.current.scrollWidth > tabNameRef.current.clientWidth; + if (lastOverflowStateRef.current !== hasOverflow) { + lastOverflowStateRef.current = hasOverflow; + setHasOverflow(hasOverflow); + } + } + }; + + const timeoutId = setTimeout(checkOverflow, 0); + const resizeObserver = new ResizeObserver(() => { + checkOverflow(); + }); + + if (tabNameRef.current) { + resizeObserver.observe(tabNameRef.current); + } + + return () => { + clearTimeout(timeoutId); + resizeObserver.disconnect(); + }; + }, [item, item?.name, method, setHasOverflow]); + const handleCloseClick = (event) => { event.stopPropagation(); event.preventDefault(); @@ -105,11 +148,12 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi const hasDraft = tab.type === 'collection-settings' && collection?.draft; const hasFolderDraft = tab.type === 'folder-settings' && folder?.draft; + if (['collection-settings', 'collection-overview', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) { return ( {showConfirmCollectionClose && tab.type === 'collection-settings' && ( { - if (!item) return; - - switch (item.type) { - case 'grpc-request': - return 'gRPC'; - case 'ws-request': - return 'WS'; - case 'graphql-request': - return 'GQL'; - default: - return item.draft ? get(item, 'draft.request.method') : get(item, 'request.method'); - } - }, [item]); - const hasChanges = useMemo(() => hasRequestChanges(item), [item]); if (!item) { @@ -228,10 +257,36 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi } const isWS = item.type === 'ws-request'; - const method = getMethodText(item); + + useEffect(() => { + const checkOverflow = () => { + if (tabNameRef.current && setHasOverflow) { + const hasOverflow = tabNameRef.current.scrollWidth > tabNameRef.current.clientWidth; + if (lastOverflowStateRef.current !== hasOverflow) { + lastOverflowStateRef.current = hasOverflow; + setHasOverflow(hasOverflow); + } + } + }; + + const timeoutId = setTimeout(checkOverflow, 0); + + const resizeObserver = new ResizeObserver(() => { + checkOverflow(); + }); + + if (tabNameRef.current) { + resizeObserver.observe(tabNameRef.current); + } + + return () => { + clearTimeout(timeoutId); + resizeObserver.disconnect(); + }; + }, [item.name, method, setHasOverflow]); return ( - + {showConfirmClose && ( )}
dispatch(makeTabPermanent({ uid: tab.uid }))} onMouseUp={(e) => { @@ -284,7 +339,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi {method} - + {item.name}
-
{ if (!hasChanges) { isWS && closeWsConnection(item.uid); return handleCloseClick(e); - }; + } e.stopPropagation(); e.preventDefault(); setShowConfirmClose(true); }} - > - {!hasChanges ? ( - - ) : ( - - )} -
+ />
); }; @@ -349,7 +398,7 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col } dispatch(closeTabs({ tabUids: [tabUid] })); - } catch (err) {} + } catch (err) { } } function handleRevertChanges(event) { @@ -368,7 +417,7 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col collectionUid: collection.uid })); } - } catch (err) {} + } catch (err) { } } function handleCloseOtherTabs(event) { diff --git a/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js b/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js index f555615da..0e042152b 100644 --- a/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js @@ -1,13 +1,44 @@ import styled from 'styled-components'; const Wrapper = styled.div` - border-bottom: 1px solid ${(props) => props.theme.requestTabs.bottomBorder}; + position: relative; + + &::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background: ${(props) => props.theme.requestTabs.bottomBorder}; + z-index: 0; + } + + .tabs-scroll-container { + overflow-x: auto; + overflow-y: clip; + padding-bottom: 10px; + margin-bottom: -10px; + + &::-webkit-scrollbar { + display: none; + } + + scrollbar-width: none; + + ul { + margin-bottom: 0; + overflow: visible; + } + } ul { - padding: 0; + padding: 0 2px; margin: 0; display: flex; - overflow: scroll; + align-items: flex-end; + position: relative; + z-index: 1; &::-webkit-scrollbar { display: none; @@ -17,57 +48,128 @@ const Wrapper = styled.div` li { display: inline-flex; - max-width: 150px; - border: 1px solid transparent; + max-width: 180px; + min-width: 80px; list-style: none; - padding-top: 8px; - padding-bottom: 8px; - padding-left: 0; - padding-right: 0; cursor: pointer; - font-size: ${(props) => props.theme.font.size.base}; - height: 38px; - - margin-right: 6px; + font-size: 0.8125rem; + position: relative; + margin-right: 3px; color: ${(props) => props.theme.requestTabs.color}; - background: ${(props) => props.theme.requestTabs.bg}; - border-radius: 0; + background: transparent; + border: 1px solid transparent; + padding: 6px 0; + flex-shrink: 0; + transition: background-color 0.15s ease; + margin-bottom: 4px; .tab-container { width: 100%; + position: relative; + overflow: hidden; + } + + &:not(.active) { + background: ${(props) => props.theme.requestTabs.bg}; + border-color: transparent; + border-radius: ${(props) => props.theme.border.radius.base}; + + } + + &:nth-last-child(1) { + margin-right: 10px; + } + + &.has-overflow:not(:hover) .tab-name { + mask-image: linear-gradient( + to right, + ${(props) => props.theme.requestTabs.color} 0%, + ${(props) => props.theme.requestTabs.color} calc(100% - 24px), + transparent 100% + ); + -webkit-mask-image: linear-gradient( + to right, + ${(props) => props.theme.requestTabs.color} 0%, + ${(props) => props.theme.requestTabs.color} calc(100% - 24px), + transparent 100% + ); + } + + &.has-overflow:hover .tab-name { + mask-image: linear-gradient( + to right, + ${(props) => props.theme.requestTabs.color} 0%, + ${(props) => props.theme.requestTabs.color} calc(100% - 8px), + transparent 100% + ); + -webkit-mask-image: linear-gradient( + to right, + ${(props) => props.theme.requestTabs.color} 0%, + ${(props) => props.theme.requestTabs.color} calc(100% - 8px), + transparent 100% + ); } &.active { - background: ${(props) => props.theme.requestTabs.active.bg}; - } + background: ${(props) => props.theme.bg || '#ffffff'}; + border: 1px solid ${(props) => props.theme.requestTabs.bottomBorder}; + border-bottom-color: ${(props) => props.theme.bg || '#ffffff'}; + border-radius: 8px 8px 0 0; + font-weight: 500; + z-index: 2; + margin-bottom: -2px; + padding-bottom: 12px; - &.active { - .close-icon-container .close-icon { - display: block; + &::before { + content: ''; + position: absolute; + bottom: -1px; + left: -8px; + width: 8px; + height: 8px; + background: transparent; + border-bottom-right-radius: 8px; + box-shadow: 2px 2px 0 0 ${(props) => props.theme.bg || '#ffffff'}; + border-right: 1px solid ${(props) => props.theme.requestTabs.bottomBorder}; + border-bottom: 1px solid ${(props) => props.theme.requestTabs.bottomBorder}; } - } - &:hover { - .close-icon-container .close-icon { - display: block; + &::after { + content: ''; + position: absolute; + bottom: -1px; + right: -8px; + width: 8px; + height: 8px; + background: transparent; + border-bottom-left-radius: 8px; + box-shadow: -2px 2px 0 0 ${(props) => props.theme.bg || '#ffffff'}; + border-left: 1px solid ${(props) => props.theme.requestTabs.bottomBorder}; + border-bottom: 1px solid ${(props) => props.theme.requestTabs.bottomBorder}; } } &.short-tab { - vertical-align: bottom; - width: 34px; - min-width: 34px; - max-width: 34px; - padding: 3px 0px; + width: 32px; + min-width: 32px; + max-width: 32px; + padding: 5px 0; display: inline-flex; justify-content: center; + align-items: center; color: ${(props) => props.theme.requestTabs.shortTab.color}; - background-color: ${(props) => props.theme.requestTabs.shortTab.bg}; - position: relative; - top: -1px; + background-color: transparent; + border: 1px solid transparent; + border-radius: ${(props) => props.theme.border.radius.base}; + flex-shrink: 0; > div { - padding: 3px 4px; + padding: 3px; + display: flex; + align-items: center; + justify-content: center; + border-radius: ${(props) => props.theme.border.radius.sm}; + transition: background-color 0.12s ease, color 0.12s ease; } > div.home-icon-container { @@ -81,19 +183,23 @@ const Wrapper = styled.div` } svg { - height: 22px; + height: 20px; + width: 20px; } &:hover { > div { background-color: ${(props) => props.theme.requestTabs.shortTab.hoverBg}; color: ${(props) => props.theme.requestTabs.shortTab.hoverColor}; - border-radius: 3px; } } } } } + + &.has-chevrons ul { + padding-left: 0; + } `; export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestTabs/index.js b/packages/bruno-app/src/components/RequestTabs/index.js index c9fbb74aa..871258c42 100644 --- a/packages/bruno-app/src/components/RequestTabs/index.js +++ b/packages/bruno-app/src/components/RequestTabs/index.js @@ -1,4 +1,4 @@ -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useEffect, useCallback } from 'react'; import find from 'lodash/find'; import filter from 'lodash/filter'; import classnames from 'classnames'; @@ -14,7 +14,10 @@ import DraggableTab from './DraggableTab'; const RequestTabs = () => { const dispatch = useDispatch(); const tabsRef = useRef(); + const scrollContainerRef = useRef(); const [newRequestModalOpen, setNewRequestModalOpen] = useState(false); + const [tabOverflowStates, setTabOverflowStates] = useState({}); + const [showChevrons, setShowChevrons] = useState(false); const tabs = useSelector((state) => state.tabs.tabs); const activeTabUid = useSelector((state) => state.tabs.activeTabUid); const collections = useSelector((state) => state.collections.collections); @@ -22,10 +25,48 @@ const RequestTabs = () => { const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed); const screenWidth = useSelector((state) => state.app.screenWidth); + const createSetHasOverflow = useCallback((tabUid) => { + return (hasOverflow) => { + setTabOverflowStates((prev) => { + if (prev[tabUid] === hasOverflow) { + return prev; + } + return { + ...prev, + [tabUid]: hasOverflow + }; + }); + }; + }, []); + + const activeTab = find(tabs, (t) => t.uid === activeTabUid); + const activeCollection = find(collections, (c) => c.uid === activeTab?.collectionUid); + const collectionRequestTabs = filter(tabs, (t) => t.collectionUid === activeTab?.collectionUid); + + useEffect(() => { + if (!activeTabUid || !activeTab) return; + + const checkOverflow = () => { + if (tabsRef.current && scrollContainerRef.current) { + const hasOverflow = tabsRef.current.scrollWidth > scrollContainerRef.current.clientWidth; + setShowChevrons(hasOverflow); + } + }; + + checkOverflow(); + const resizeObserver = new ResizeObserver(checkOverflow); + if (scrollContainerRef.current) { + resizeObserver.observe(scrollContainerRef.current); + } + + return () => resizeObserver.disconnect(); + }, [activeTabUid, activeTab, collectionRequestTabs.length, screenWidth, leftSidebarWidth, sidebarCollapsed]); + const getTabClassname = (tab, index) => { return classnames('request-tab select-none', { 'active': tab.uid === activeTabUid, - 'last-tab': tabs && tabs.length && index === tabs.length - 1 + 'last-tab': tabs && tabs.length && index === tabs.length - 1, + 'has-overflow': tabOverflowStates[tab.uid] }); }; @@ -43,31 +84,22 @@ const RequestTabs = () => { return null; } - const activeTab = find(tabs, (t) => t.uid === activeTabUid); if (!activeTab) { return Something went wrong!; } - const activeCollection = find(collections, (c) => c.uid === activeTab.collectionUid); - const collectionRequestTabs = filter(tabs, (t) => t.collectionUid === activeTab.collectionUid); - const effectiveSidebarWidth = sidebarCollapsed ? 0 : leftSidebarWidth; const maxTablistWidth = screenWidth - effectiveSidebarWidth - 150; - const tabsWidth = collectionRequestTabs.length * 150 + 34; // 34: (+)icon - const showChevrons = maxTablistWidth < tabsWidth; const leftSlide = () => { - tabsRef.current.scrollBy({ + scrollContainerRef.current?.scrollBy({ left: -120, behavior: 'smooth' }); }; - // todo: bring new tab to focus if its not in focus - // tabsRef.current.scrollLeft - const rightSlide = () => { - tabsRef.current.scrollBy({ + scrollContainerRef.current?.scrollBy({ left: 120, behavior: 'smooth' }); @@ -87,7 +119,7 @@ const RequestTabs = () => { {collectionRequestTabs && collectionRequestTabs.length ? ( <> -
+
    {showChevrons ? (
  • @@ -103,36 +135,40 @@ const RequestTabs = () => {
*/} -
    - {collectionRequestTabs && collectionRequestTabs.length - ? collectionRequestTabs.map((tab, index) => { - return ( - { - dispatch(reorderTabs({ - sourceUid: source, - targetUid: target - })); - }} - className={getTabClassname(tab, index)} - onClick={() => handleClick(tab)} - > - +
      + {collectionRequestTabs && collectionRequestTabs.length + ? collectionRequestTabs.map((tab, index) => { + return ( + - - ); - }) - : null} -
    + id={tab.uid} + index={index} + onMoveTab={(source, target) => { + dispatch(reorderTabs({ + sourceUid: source, + targetUid: target + })); + }} + className={getTabClassname(tab, index)} + onClick={() => handleClick(tab)} + > + +
    + ); + }) + : null} +
+
    {showChevrons ? ( diff --git a/tests/grpc/method-search/grpc-method-search.spec.ts b/tests/grpc/method-search/grpc-method-search.spec.ts index 6c1274c27..ffa9c83d1 100644 --- a/tests/grpc/method-search/grpc-method-search.spec.ts +++ b/tests/grpc/method-search/grpc-method-search.spec.ts @@ -15,7 +15,7 @@ test.describe('Grpc Collection - Method Search Functionality', () => { test.afterEach(async ({ pageWithUserData: page }) => { await test.step('Close the gRPC sayHello tab without saving changes', async () => { - await page.getByRole('tab', { name: 'gRPC sayHello' }).getByRole('img').click(); + await page.getByRole('tab', { name: 'gRPC sayHello' }).getByTestId('request-tab-close-icon').click(); await page.getByRole('button', { name: 'Don\'t Save' }).click(); }); }); diff --git a/tests/preferences/autosave/autosave.spec.ts b/tests/preferences/autosave/autosave.spec.ts index a7e13a7b4..aa7ff304c 100644 --- a/tests/preferences/autosave/autosave.spec.ts +++ b/tests/preferences/autosave/autosave.spec.ts @@ -59,18 +59,19 @@ test.describe('Autosave', () => { await page.keyboard.press('End'); await page.keyboard.type('/users'); - // Verify draft indicator appears + // Wait for draft indicator to appear (change registered) const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Test Request' }) }); await expect(requestTab.locator('.has-changes-icon')).toBeVisible(); - // Verify draft indicator disappears after autosave + // Wait for autosave to complete (draft indicator disappears) await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible({ timeout: 5000 }); }); await test.step('Verify changes persisted', async () => { // Close and reopen the request tab to verify persistence const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Test Request' }) }); - await requestTab.locator('.close-icon').click(); + await requestTab.hover(); + await requestTab.getByTestId('request-tab-close-icon').click(); // Reopen request await page.locator('.collection-item-name').filter({ hasText: 'Test Request' }).click(); @@ -107,6 +108,9 @@ test.describe('Autosave', () => { await page.keyboard.press('End'); await page.keyboard.type('/posts'); + // Move mouse away from tab to ensure draft icon is visible (hover shows close icon) + await page.mouse.move(0, 0); + // Verify draft indicator appears const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Test Request' }) }); await expect(requestTab.locator('.has-changes-icon')).toBeVisible(); @@ -149,6 +153,9 @@ test.describe('Autosave', () => { await page.keyboard.press('End'); await page.keyboard.type('/existing-draft'); + // Move mouse away from tab to ensure draft icon is visible (hover shows close icon) + await page.mouse.move(0, 0); + // Verify draft indicator appears const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Draft Request' }) }); await expect(requestTab.locator('.has-changes-icon')).toBeVisible(); @@ -180,7 +187,8 @@ test.describe('Autosave', () => { await test.step('Verify changes persisted', async () => { // Close and reopen the request tab to verify persistence const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Draft Request' }) }); - await requestTab.locator('.close-icon').click(); + await requestTab.hover(); + await requestTab.getByTestId('request-tab-close-icon').click(); // Reopen request await page.locator('.collection-item-name').filter({ hasText: 'Draft Request' }).click(); diff --git a/tests/protobuf/manage-protofile.spec.ts b/tests/protobuf/manage-protofile.spec.ts index 5d9895745..e96d17019 100644 --- a/tests/protobuf/manage-protofile.spec.ts +++ b/tests/protobuf/manage-protofile.spec.ts @@ -35,19 +35,22 @@ test.describe('manage protofile', () => { const protoFilesTable = page.getByTestId('protobuf-proto-files-table'); await expect(protoFilesTable).toBeVisible(); + // Wait for table data to load by checking for a known cell const file = page.getByRole('cell', { name: 'product.proto', exact: true }); - expect(file).toBeVisible(); + await expect(file).toBeVisible(); const filePath = page.getByRole('cell', { name: '../protos/services/product.proto' }); - expect(filePath).toBeVisible(); + await expect(filePath).toBeVisible(); // Check import paths table const importPathsTable = page.getByTestId('protobuf-import-paths-table'); await expect(importPathsTable).toBeVisible(); + // Wait for import paths table data to load const importPath = page.getByRole('cell', { name: '../protos/types', exact: true }); await expect(importPath).toBeVisible(); + // Wait for invalid file path cell to appear const invalidFilePath = page.getByRole('cell', { name: 'invalid-file-path.proto', exact: true }); await expect(invalidFilePath).toBeVisible(); @@ -64,6 +67,7 @@ test.describe('manage protofile', () => { await expect(invalidProtoFilesMessage).toBeVisible(); await expect(invalidImportPathsMessage).toBeVisible(); + // Wait for collection path cells to appear await expect(collectionPathAsImportPath).toBeVisible(); await expect(collectionPathName).toBeVisible(); @@ -102,7 +106,9 @@ test.describe('manage protofile', () => { const method = page.getByTestId('grpc-method-item').filter({ hasText: /^CreateOrderunary$/ }).first(); await expect(method).toBeVisible(); await method.click(); - await page.getByRole('tab', { name: 'gRPC sayHello' }).getByRole('img').click(); + const requestTab = page.getByRole('tab', { name: 'gRPC sayHello' }); + await requestTab.hover(); + await requestTab.getByTestId('request-tab-close-icon').click(); await page.getByRole('button', { name: 'Don\'t Save' }).click(); }); @@ -128,7 +134,9 @@ test.describe('manage protofile', () => { const methodsDropdown = page.getByTestId('grpc-methods-dropdown'); await expect(methodsDropdown).not.toBeVisible(); - await page.getByRole('tab', { name: 'gRPC sayHello' }).getByRole('img').click(); + const requestTab = page.getByRole('tab', { name: 'gRPC sayHello' }); + await requestTab.hover(); + await requestTab.getByTestId('request-tab-close-icon').click(); await page.getByRole('button', { name: 'Don\'t Save' }).click(); }); @@ -170,7 +178,9 @@ test.describe('manage protofile', () => { await method.click(); // Clean up - await page.getByRole('tab', { name: 'gRPC sayHello' }).getByRole('img').click(); + const requestTab = page.getByRole('tab', { name: 'gRPC sayHello' }); + await requestTab.hover(); + await requestTab.getByTestId('request-tab-close-icon').click(); await page.getByRole('button', { name: 'Don\'t Save' }).click(); }); });