mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-01 16:44:16 +00:00
@@ -108,33 +108,6 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.workspace.border};
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
padding: 8px 0;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.text};
|
||||
border-bottom-color: ${(props) => props.theme.workspace.accent};
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -7,13 +7,16 @@ import toast from 'react-hot-toast';
|
||||
import CloseWorkspace from 'components/Sidebar/CloseWorkspace';
|
||||
import WorkspaceOverview from './WorkspaceOverview';
|
||||
import WorkspaceEnvironments from './WorkspaceEnvironments';
|
||||
import WorkspaceTabs from 'components/WorkspaceTabs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
|
||||
const WorkspaceHome = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
const workspaceTabs = useSelector((state) => state.workspaceTabs);
|
||||
const activeTabUid = workspaceTabs.activeTabUid;
|
||||
const activeTab = workspaceTabs.tabs.find((t) => t.uid === activeTabUid);
|
||||
|
||||
const [isRenamingWorkspace, setIsRenamingWorkspace] = useState(false);
|
||||
const [workspaceNameInput, setWorkspaceNameInput] = useState('');
|
||||
@@ -143,13 +146,10 @@ const WorkspaceHome = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'overview', label: 'Overview' },
|
||||
{ id: 'environments', label: 'Global Environments' }
|
||||
];
|
||||
|
||||
const renderTabContent = () => {
|
||||
switch (activeTab) {
|
||||
if (!activeTab) return null;
|
||||
|
||||
switch (activeTab.type) {
|
||||
case 'overview':
|
||||
return <WorkspaceOverview workspace={activeWorkspace} />;
|
||||
case 'environments':
|
||||
@@ -244,17 +244,7 @@ const WorkspaceHome = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="tabs-container">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
className={`tab-item ${activeTab === tab.id ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<WorkspaceTabs workspaceUid={activeWorkspace.uid} />
|
||||
|
||||
<div className="tab-content">{renderTabContent()}</div>
|
||||
</div>
|
||||
|
||||
198
packages/bruno-app/src/components/WorkspaceTabs/StyledWrapper.js
Normal file
198
packages/bruno-app/src/components/WorkspaceTabs/StyledWrapper.js
Normal file
@@ -0,0 +1,198 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
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 3px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
position: relative;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
scrollbar-width: none;
|
||||
|
||||
li {
|
||||
display: inline-flex;
|
||||
max-width: 180px;
|
||||
min-width: 80px;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
font-size: 0.8125rem;
|
||||
position: relative;
|
||||
margin-right: 3px;
|
||||
color: ${(props) => props.theme.requestTabs.color};
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
padding: 6px 0;
|
||||
flex-shrink: 0;
|
||||
transition: background-color 0.15s ease;
|
||||
margin-bottom: 3px;
|
||||
|
||||
.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: 4px;
|
||||
}
|
||||
|
||||
&.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% - 12px),
|
||||
transparent 100%
|
||||
);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to right,
|
||||
${(props) => props.theme.requestTabs.color} 0%,
|
||||
${(props) => props.theme.requestTabs.color} calc(100% - 12px),
|
||||
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.bg || '#ffffff'};
|
||||
border: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
border-bottom-color: ${(props) => props.theme.bg || '#ffffff'};
|
||||
border-radius: 8px 8px 0 0;
|
||||
z-index: 2;
|
||||
margin-bottom: -2px;
|
||||
padding-bottom: 12px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
left: -8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: transparent;
|
||||
border-bottom-right-radius: 6px;
|
||||
box-shadow: 3px 3px 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};
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
right: -8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: transparent;
|
||||
border-bottom-left-radius: 6px;
|
||||
box-shadow: -3px 3px 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};
|
||||
}
|
||||
}
|
||||
|
||||
&.permanent-tab {
|
||||
.close-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.short-tab {
|
||||
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: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
flex-shrink: 0;
|
||||
|
||||
> div {
|
||||
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;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
> div {
|
||||
background-color: ${(props) => props.theme.requestTabs.shortTab.hoverBg};
|
||||
color: ${(props) => props.theme.requestTabs.shortTab.hoverColor};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.has-chevrons ul {
|
||||
padding-left: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,61 @@
|
||||
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-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 6px;
|
||||
color: ${(props) => props.theme.requestTabs.color};
|
||||
}
|
||||
|
||||
.tab-name {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
font-size: 0.8125rem;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
margin-left: 6px;
|
||||
padding: 2px;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s, background-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.requestTabs.closeIconHoverBg || 'rgba(0, 0, 0, 0.1)'};
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .close-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.permanent .close-icon {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { IconX, IconHome, IconWorld } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { closeWorkspaceTab } from 'providers/ReduxStore/slices/workspaceTabs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const TAB_ICONS = {
|
||||
overview: IconHome,
|
||||
environments: IconWorld
|
||||
};
|
||||
|
||||
const WorkspaceTab = ({ tab, isActive }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleCloseClick = (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
dispatch(closeWorkspaceTab({ uid: tab.uid }));
|
||||
};
|
||||
|
||||
const TabIcon = TAB_ICONS[tab.type];
|
||||
|
||||
return (
|
||||
<StyledWrapper className={`flex items-center justify-between tab-container px-2 ${tab.permanent ? 'permanent' : ''}`}>
|
||||
<div className="flex items-center tab-label">
|
||||
{TabIcon && (
|
||||
<span className="tab-icon">
|
||||
<TabIcon size={14} strokeWidth={1.5} />
|
||||
</span>
|
||||
)}
|
||||
<span className="tab-name" title={tab.label}>
|
||||
{tab.label}
|
||||
</span>
|
||||
</div>
|
||||
{!tab.permanent && (
|
||||
<div className="close-icon" onClick={handleCloseClick}>
|
||||
<IconX size={14} strokeWidth={1.5} />
|
||||
</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceTab;
|
||||
158
packages/bruno-app/src/components/WorkspaceTabs/index.js
Normal file
158
packages/bruno-app/src/components/WorkspaceTabs/index.js
Normal file
@@ -0,0 +1,158 @@
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import filter from 'lodash/filter';
|
||||
import classnames from 'classnames';
|
||||
import { IconChevronRight, IconChevronLeft } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { focusWorkspaceTab, initializeWorkspaceTabs } from 'providers/ReduxStore/slices/workspaceTabs';
|
||||
import WorkspaceTab from './WorkspaceTab';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const PERMANENT_TABS = [
|
||||
{ type: 'overview', label: 'Overview' },
|
||||
{ type: 'environments', label: 'Global Environments' }
|
||||
];
|
||||
|
||||
const WorkspaceTabs = ({ workspaceUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabsRef = useRef();
|
||||
const scrollContainerRef = useRef();
|
||||
const [tabOverflowStates, setTabOverflowStates] = useState({});
|
||||
const [showChevrons, setShowChevrons] = useState(false);
|
||||
|
||||
const tabs = useSelector((state) => state.workspaceTabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.workspaceTabs.activeTabUid);
|
||||
const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth);
|
||||
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
|
||||
const screenWidth = useSelector((state) => state.app.screenWidth);
|
||||
|
||||
// Initialize permanent tabs for this workspace
|
||||
useEffect(() => {
|
||||
if (workspaceUid) {
|
||||
dispatch(initializeWorkspaceTabs({
|
||||
workspaceUid,
|
||||
permanentTabs: PERMANENT_TABS
|
||||
}));
|
||||
}
|
||||
}, [workspaceUid, dispatch]);
|
||||
|
||||
const createSetHasOverflow = useCallback((tabUid) => {
|
||||
return (hasOverflow) => {
|
||||
setTabOverflowStates((prev) => {
|
||||
if (prev[tabUid] === hasOverflow) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[tabUid]: hasOverflow
|
||||
};
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Filter tabs for this workspace
|
||||
const workspaceTabs = filter(tabs, (t) => t.workspaceUid === workspaceUid);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeTabUid) 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, workspaceTabs.length, screenWidth, leftSidebarWidth, sidebarCollapsed]);
|
||||
|
||||
const getTabClassname = (tab, index) => {
|
||||
return classnames('request-tab select-none', {
|
||||
'active': tab.uid === activeTabUid,
|
||||
'permanent-tab': tab.permanent,
|
||||
'last-tab': workspaceTabs && workspaceTabs.length && index === workspaceTabs.length - 1,
|
||||
'has-overflow': tabOverflowStates[tab.uid]
|
||||
});
|
||||
};
|
||||
|
||||
const handleClick = (tab) => {
|
||||
dispatch(focusWorkspaceTab({ uid: tab.uid }));
|
||||
};
|
||||
|
||||
if (!workspaceUid || workspaceTabs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const effectiveSidebarWidth = sidebarCollapsed ? 0 : leftSidebarWidth;
|
||||
const maxTablistWidth = screenWidth - effectiveSidebarWidth - 150;
|
||||
|
||||
const leftSlide = () => {
|
||||
scrollContainerRef.current?.scrollBy({
|
||||
left: -120,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
};
|
||||
|
||||
const rightSlide = () => {
|
||||
scrollContainerRef.current?.scrollBy({
|
||||
left: 120,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
};
|
||||
|
||||
const getRootClassname = () => {
|
||||
return classnames({
|
||||
'has-chevrons': showChevrons
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className={getRootClassname()}>
|
||||
<div className="flex items-center pl-2">
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={leftSlide}>
|
||||
<div className="flex items-center">
|
||||
<IconChevronLeft size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
<div className="tabs-scroll-container" style={{ maxWidth: maxTablistWidth }} ref={scrollContainerRef}>
|
||||
<ul role="tablist" ref={tabsRef}>
|
||||
{workspaceTabs.map((tab, index) => (
|
||||
<li
|
||||
key={tab.uid}
|
||||
className={getTabClassname(tab, index)}
|
||||
onClick={() => handleClick(tab)}
|
||||
>
|
||||
<WorkspaceTab
|
||||
tab={tab}
|
||||
isActive={tab.uid === activeTabUid}
|
||||
hasOverflow={tabOverflowStates[tab.uid]}
|
||||
setHasOverflow={createSetHasOverflow(tab.uid)}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={rightSlide}>
|
||||
<div className="flex items-center">
|
||||
<IconChevronRight size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceTabs;
|
||||
@@ -4,6 +4,7 @@ import debugMiddleware from './middlewares/debug/middleware';
|
||||
import appReducer from './slices/app';
|
||||
import collectionsReducer from './slices/collections';
|
||||
import tabsReducer from './slices/tabs';
|
||||
import workspaceTabsReducer from './slices/workspaceTabs';
|
||||
import notificationsReducer from './slices/notifications';
|
||||
import globalEnvironmentsReducer from './slices/global-environments';
|
||||
import logsReducer from './slices/logs';
|
||||
@@ -27,6 +28,7 @@ export const store = configureStore({
|
||||
app: appReducer,
|
||||
collections: collectionsReducer,
|
||||
tabs: tabsReducer,
|
||||
workspaceTabs: workspaceTabsReducer,
|
||||
notifications: notificationsReducer,
|
||||
globalEnvironments: globalEnvironmentsReducer,
|
||||
logs: logsReducer,
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import filter from 'lodash/filter';
|
||||
import find from 'lodash/find';
|
||||
import last from 'lodash/last';
|
||||
|
||||
const initialState = {
|
||||
tabs: [],
|
||||
activeTabUid: null
|
||||
};
|
||||
|
||||
export const workspaceTabsSlice = createSlice({
|
||||
name: 'workspaceTabs',
|
||||
initialState,
|
||||
reducers: {
|
||||
addWorkspaceTab: (state, action) => {
|
||||
const { uid, workspaceUid, type, label, permanent = false } = action.payload;
|
||||
|
||||
const existingTab = find(state.tabs, (tab) => tab.uid === uid);
|
||||
if (existingTab) {
|
||||
state.activeTabUid = existingTab.uid;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if a tab of the same type already exists for this workspace
|
||||
const existingTypeTab = find(
|
||||
state.tabs,
|
||||
(tab) => tab.workspaceUid === workspaceUid && tab.type === type
|
||||
);
|
||||
if (existingTypeTab) {
|
||||
state.activeTabUid = existingTypeTab.uid;
|
||||
return;
|
||||
}
|
||||
|
||||
state.tabs.push({
|
||||
uid,
|
||||
workspaceUid,
|
||||
type,
|
||||
label,
|
||||
permanent
|
||||
});
|
||||
state.activeTabUid = uid;
|
||||
},
|
||||
focusWorkspaceTab: (state, action) => {
|
||||
state.activeTabUid = action.payload.uid;
|
||||
},
|
||||
closeWorkspaceTab: (state, action) => {
|
||||
const tabUid = action.payload.uid;
|
||||
const tab = find(state.tabs, (t) => t.uid === tabUid);
|
||||
|
||||
// Don't allow closing permanent tabs
|
||||
if (tab?.permanent) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.tabs = filter(state.tabs, (t) => t.uid !== tabUid);
|
||||
|
||||
// If we closed the active tab, activate another one
|
||||
if (state.activeTabUid === tabUid && state.tabs.length > 0) {
|
||||
state.activeTabUid = last(state.tabs).uid;
|
||||
} else if (state.tabs.length === 0) {
|
||||
state.activeTabUid = null;
|
||||
}
|
||||
},
|
||||
closeWorkspaceTabs: (state, action) => {
|
||||
const tabUids = action.payload.tabUids || [];
|
||||
|
||||
// Filter out permanent tabs from the close request
|
||||
const tabsToClose = tabUids.filter((uid) => {
|
||||
const tab = find(state.tabs, (t) => t.uid === uid);
|
||||
return tab && !tab.permanent;
|
||||
});
|
||||
|
||||
state.tabs = filter(state.tabs, (t) => !tabsToClose.includes(t.uid));
|
||||
|
||||
// If active tab was closed, activate another one
|
||||
if (tabsToClose.includes(state.activeTabUid)) {
|
||||
if (state.tabs.length > 0) {
|
||||
state.activeTabUid = last(state.tabs).uid;
|
||||
} else {
|
||||
state.activeTabUid = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
closeAllWorkspaceTabs: (state, action) => {
|
||||
const workspaceUid = action.payload?.workspaceUid;
|
||||
|
||||
if (workspaceUid) {
|
||||
// Close non-permanent tabs for specific workspace
|
||||
state.tabs = filter(
|
||||
state.tabs,
|
||||
(t) => t.workspaceUid !== workspaceUid || t.permanent
|
||||
);
|
||||
} else {
|
||||
// Close all non-permanent tabs
|
||||
state.tabs = filter(state.tabs, (t) => t.permanent);
|
||||
}
|
||||
|
||||
// If active tab was closed, activate another one
|
||||
const activeTabExists = find(state.tabs, (t) => t.uid === state.activeTabUid);
|
||||
if (!activeTabExists) {
|
||||
state.activeTabUid = state.tabs.length > 0 ? last(state.tabs).uid : null;
|
||||
}
|
||||
},
|
||||
reorderWorkspaceTabs: (state, action) => {
|
||||
const { sourceUid, targetUid } = action.payload;
|
||||
const tabs = state.tabs;
|
||||
|
||||
const sourceIdx = tabs.findIndex((t) => t.uid === sourceUid);
|
||||
const targetIdx = tabs.findIndex((t) => t.uid === targetUid);
|
||||
|
||||
// Don't reorder permanent tabs
|
||||
const sourceTab = tabs[sourceIdx];
|
||||
if (sourceTab?.permanent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourceIdx < 0 || targetIdx < 0 || sourceIdx === targetIdx) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [moved] = tabs.splice(sourceIdx, 1);
|
||||
tabs.splice(targetIdx, 0, moved);
|
||||
|
||||
state.tabs = tabs;
|
||||
},
|
||||
initializeWorkspaceTabs: (state, action) => {
|
||||
const { workspaceUid, permanentTabs } = action.payload;
|
||||
|
||||
// Check if permanent tabs already exist for this workspace
|
||||
const existingPermanentTabs = state.tabs.filter(
|
||||
(t) => t.workspaceUid === workspaceUid && t.permanent
|
||||
);
|
||||
|
||||
if (existingPermanentTabs.length === 0) {
|
||||
// Add permanent tabs
|
||||
permanentTabs.forEach((tab) => {
|
||||
state.tabs.push({
|
||||
uid: `${workspaceUid}-${tab.type}`,
|
||||
workspaceUid,
|
||||
type: tab.type,
|
||||
label: tab.label,
|
||||
permanent: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const workspaceActiveTab = state.tabs.find(
|
||||
(t) => t.uid === state.activeTabUid && t.workspaceUid === workspaceUid
|
||||
);
|
||||
|
||||
if (!workspaceActiveTab) {
|
||||
const workspaceTabs = state.tabs.filter((t) => t.workspaceUid === workspaceUid);
|
||||
if (workspaceTabs.length > 0) {
|
||||
state.activeTabUid = workspaceTabs[0].uid;
|
||||
}
|
||||
}
|
||||
},
|
||||
setActiveWorkspaceTab: (state, action) => {
|
||||
const { workspaceUid, type } = action.payload;
|
||||
let tab = find(
|
||||
state.tabs,
|
||||
(t) => t.workspaceUid === workspaceUid && t.type === type
|
||||
);
|
||||
|
||||
if (!tab) {
|
||||
const newTabUid = `${workspaceUid}-${type}`;
|
||||
const newTab = {
|
||||
uid: newTabUid,
|
||||
workspaceUid,
|
||||
type,
|
||||
label: type === 'overview' ? 'Overview' : type,
|
||||
permanent: false
|
||||
};
|
||||
state.tabs.push(newTab);
|
||||
tab = newTab;
|
||||
}
|
||||
|
||||
state.activeTabUid = tab.uid;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const {
|
||||
addWorkspaceTab,
|
||||
focusWorkspaceTab,
|
||||
closeWorkspaceTab,
|
||||
closeWorkspaceTabs,
|
||||
closeAllWorkspaceTabs,
|
||||
reorderWorkspaceTabs,
|
||||
initializeWorkspaceTabs,
|
||||
setActiveWorkspaceTab
|
||||
} = workspaceTabsSlice.actions;
|
||||
|
||||
export default workspaceTabsSlice.reducer;
|
||||
@@ -12,6 +12,7 @@ import { showHomePage } from '../app';
|
||||
import { createCollection, openCollection, openMultipleCollections } from '../collections/actions';
|
||||
import { removeCollection } from '../collections';
|
||||
import { updateGlobalEnvironments } from '../global-environments';
|
||||
import { initializeWorkspaceTabs, setActiveWorkspaceTab } from '../workspaceTabs';
|
||||
import { normalizePath } from 'utils/common/path';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
@@ -253,6 +254,13 @@ export const switchWorkspace = (workspaceUid) => {
|
||||
|
||||
await loadWorkspaceCollectionsForSwitch(dispatch, workspace);
|
||||
dispatch(showHomePage());
|
||||
|
||||
const permanentTabs = [
|
||||
{ type: 'overview', label: 'Overview' },
|
||||
{ type: 'environments', label: 'Global Environments' }
|
||||
];
|
||||
dispatch(initializeWorkspaceTabs({ workspaceUid, permanentTabs }));
|
||||
dispatch(setActiveWorkspaceTab({ workspaceUid, type: 'overview' }));
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user