mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-30 16:14:06 +00:00
feat(ai): mode toggle UI and add AI support for folder & collection (#8385)
This commit is contained in:
@@ -52,6 +52,10 @@ const StyledWrapper = styled.div`
|
||||
&.method-patch { color: ${(props) => props.theme.request.methods.patch}; }
|
||||
&.method-options { color: ${(props) => props.theme.request.methods.options}; }
|
||||
&.method-head { color: ${(props) => props.theme.request.methods.head}; }
|
||||
&.method-grpc { color: ${(props) => props.theme.request.grpc}; }
|
||||
&.method-ws { color: ${(props) => props.theme.request.ws}; }
|
||||
&.method-gql { color: ${(props) => props.theme.request.gql}; }
|
||||
&.method-app { color: ${(props) => props.theme.brand}; }
|
||||
}
|
||||
|
||||
.header-title {
|
||||
|
||||
@@ -33,9 +33,17 @@ import {
|
||||
updateRequestTests,
|
||||
updateRequestScript,
|
||||
updateResponseScript,
|
||||
updateRequestDocs
|
||||
updateRequestDocs,
|
||||
updateFolderRequestScript,
|
||||
updateFolderResponseScript,
|
||||
updateFolderTests,
|
||||
updateFolderDocs,
|
||||
updateCollectionRequestScript,
|
||||
updateCollectionResponseScript,
|
||||
updateCollectionTests,
|
||||
updateCollectionDocs
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import { findItemInCollection } from 'utils/collections';
|
||||
import { findItemInCollection, isItemAFolder, isItemARequest } from 'utils/collections';
|
||||
import { getAiStatus } from 'utils/ai';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -182,6 +190,20 @@ const AiChatSidebar = ({ collection }) => {
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
const activeItem = focusedTab && collection ? findItemInCollection(collection, activeTabUid) : null;
|
||||
|
||||
const aiContext = useMemo(() => {
|
||||
if (!focusedTab || !collection) return null;
|
||||
if (activeItem && (isItemARequest(activeItem) || activeItem.type === 'app')) {
|
||||
return { kind: 'request', item: activeItem, pathname: activeItem.pathname || '', name: activeItem.name || 'Untitled' };
|
||||
}
|
||||
if (activeItem && isItemAFolder(activeItem)) {
|
||||
return { kind: 'folder', folder: activeItem, pathname: activeItem.pathname || '', name: activeItem.name || 'Untitled' };
|
||||
}
|
||||
// Anything else (collection-settings, runner, variables, openapi-sync,
|
||||
// .js files in File Mode …) falls back to the collection root so the AI
|
||||
// button always opens a useful chat instead of a no-op.
|
||||
return { kind: 'collection', pathname: collection.pathname || '', name: collection.name || 'Untitled Collection' };
|
||||
}, [focusedTab, collection, activeItem]);
|
||||
|
||||
const currentChat = allChats[activeTabUid] || { messages: [], isLoading: false, error: null, historyList: [] };
|
||||
const { messages, isLoading, error, historyList, conversationId } = currentChat;
|
||||
|
||||
@@ -209,37 +231,69 @@ const AiChatSidebar = ({ collection }) => {
|
||||
try { localStorage.setItem(SELECTED_MODEL_LS_KEY, AUTO_MODEL_ID); } catch {}
|
||||
}, [availableModels, selectedModel]);
|
||||
|
||||
const requestName = activeItem?.name || 'Untitled';
|
||||
const requestMethod = activeItem?.draft
|
||||
? get(activeItem, 'draft.request.method', 'GET')
|
||||
: get(activeItem, 'request.method', 'GET');
|
||||
const requestName = aiContext?.name || activeItem?.name || 'Untitled';
|
||||
const requestMethod = useMemo(() => {
|
||||
if (aiContext?.kind === 'folder') return 'FOLDER';
|
||||
if (aiContext?.kind === 'collection') return 'ROOT';
|
||||
if (!activeItem) return 'GET';
|
||||
if (activeItem.type === 'grpc-request') return 'GRPC';
|
||||
if (activeItem.type === 'ws-request') return 'WS';
|
||||
if (activeItem.type === 'graphql-request') return 'GQL';
|
||||
if (activeItem.type === 'app') return 'APP';
|
||||
const appOn = activeItem.draft
|
||||
? get(activeItem, 'draft.app.enabled', false)
|
||||
: get(activeItem, 'app.enabled', false);
|
||||
if (appOn) return 'APP';
|
||||
return activeItem.draft
|
||||
? get(activeItem, 'draft.request.method', 'GET')
|
||||
: get(activeItem, 'request.method', 'GET');
|
||||
}, [aiContext?.kind, activeItem]);
|
||||
|
||||
// contentType drives the AI prompt, the diff target, and which entry of
|
||||
// allContent the backend treats as "active". For requests it follows the
|
||||
// request-pane tab. For folders / collections we read the settings sub-tab
|
||||
// (and the inner pre/post script split for the Script sub-tab).
|
||||
const requestPaneTab = focusedTab?.requestPaneTab;
|
||||
const scriptPaneTab = focusedTab?.scriptPaneTab;
|
||||
const contentType = useMemo(() => {
|
||||
if (aiContext?.kind === 'folder') {
|
||||
const sub = collection?.folderLevelSettingsSelectedTab?.[aiContext.folder.uid];
|
||||
if (sub === 'test') return 'tests';
|
||||
if (sub === 'docs') return 'docs';
|
||||
if (sub === 'script') return scriptPaneTab === 'post-response' ? 'post-response' : 'pre-request';
|
||||
return 'pre-request';
|
||||
}
|
||||
if (aiContext?.kind === 'collection') {
|
||||
const sub = collection?.settingsSelectedTab;
|
||||
if (sub === 'tests') return 'tests';
|
||||
if (sub === 'overview') return 'docs';
|
||||
if (sub === 'script') return scriptPaneTab === 'post-response' ? 'post-response' : 'pre-request';
|
||||
return 'pre-request';
|
||||
}
|
||||
switch (requestPaneTab) {
|
||||
case 'tests': return 'tests';
|
||||
case 'script': return 'pre-request';
|
||||
case 'script': return scriptPaneTab === 'post-response' ? 'post-response' : 'pre-request';
|
||||
case 'docs': return 'docs';
|
||||
default: return 'app';
|
||||
}
|
||||
}, [requestPaneTab]);
|
||||
}, [aiContext, collection?.folderLevelSettingsSelectedTab, collection?.settingsSelectedTab, requestPaneTab, scriptPaneTab]);
|
||||
|
||||
// Bind the chat to the active item's pathname so the history list reflects
|
||||
// this specific request and persistence keys stay stable across sessions.
|
||||
// Restoring the most recent conversation happens once per tab — if the
|
||||
// user explicitly starts a new chat, we don't auto-replace it.
|
||||
// Bind the chat to the active context's pathname so the history list
|
||||
// reflects this specific request/folder/collection and persistence keys stay
|
||||
// stable across sessions. Restoring the most recent conversation happens
|
||||
// once per tab — if the user explicitly starts a new chat, we don't
|
||||
// auto-replace it.
|
||||
const restoredOnceRef = useRef({});
|
||||
useEffect(() => {
|
||||
if (!isOpen || !activeItem || !collection) return;
|
||||
const pathname = activeItem.pathname || '';
|
||||
if (!isOpen || !aiContext || !collection) return;
|
||||
dispatch(setChatBinding({
|
||||
tabUid: activeTabUid,
|
||||
pathname,
|
||||
pathname: aiContext.pathname,
|
||||
collectionUid: collection.uid,
|
||||
contentType
|
||||
}));
|
||||
dispatch(refreshChatHistory(activeTabUid));
|
||||
}, [isOpen, activeItem?.pathname, collection?.uid, activeTabUid, contentType, dispatch]);
|
||||
}, [isOpen, aiContext?.pathname, collection?.uid, activeTabUid, contentType, dispatch]);
|
||||
|
||||
// First-open restore: if this tab has no conversation yet and there's a
|
||||
// saved one for the same file, load the most recent.
|
||||
@@ -254,42 +308,73 @@ const AiChatSidebar = ({ collection }) => {
|
||||
}, [isOpen, activeTabUid, currentChat.conversationId, currentChat.messages?.length, historyList, dispatch]);
|
||||
|
||||
const allContent = useMemo(() => {
|
||||
if (!activeItem) return {};
|
||||
const draft = activeItem.draft;
|
||||
const draftAppCode = get(activeItem, 'draft.app.code');
|
||||
if (!aiContext) return {};
|
||||
if (aiContext.kind === 'request') {
|
||||
const item = aiContext.item;
|
||||
const draft = item.draft;
|
||||
const draftAppCode = get(item, 'draft.app.code');
|
||||
return {
|
||||
'app': draftAppCode != null ? draftAppCode : get(item, 'app.code', ''),
|
||||
'tests': draft ? get(draft, 'request.tests', '') : get(item, 'request.tests', ''),
|
||||
'pre-request': draft ? get(draft, 'request.script.req', '') : get(item, 'request.script.req', ''),
|
||||
'post-response': draft ? get(draft, 'request.script.res', '') : get(item, 'request.script.res', ''),
|
||||
'docs': draft ? get(draft, 'request.docs', '') : get(item, 'request.docs', '')
|
||||
};
|
||||
}
|
||||
if (aiContext.kind === 'folder') {
|
||||
const folder = aiContext.folder;
|
||||
const root = folder.draft || folder.root || {};
|
||||
return {
|
||||
'tests': get(root, 'request.tests', ''),
|
||||
'pre-request': get(root, 'request.script.req', ''),
|
||||
'post-response': get(root, 'request.script.res', ''),
|
||||
'docs': get(root, 'docs', '')
|
||||
};
|
||||
}
|
||||
// collection
|
||||
const root = collection?.draft?.root || collection?.root || {};
|
||||
return {
|
||||
'app': draftAppCode != null ? draftAppCode : get(activeItem, 'app.code', ''),
|
||||
'tests': draft ? get(draft, 'request.tests', '') : get(activeItem, 'request.tests', ''),
|
||||
'pre-request': draft ? get(draft, 'request.script.req', '') : get(activeItem, 'request.script.req', ''),
|
||||
'post-response': draft ? get(draft, 'request.script.res', '') : get(activeItem, 'request.script.res', ''),
|
||||
'docs': draft ? get(draft, 'request.docs', '') : get(activeItem, 'request.docs', '')
|
||||
'tests': get(root, 'request.tests', ''),
|
||||
'pre-request': get(root, 'request.script.req', ''),
|
||||
'post-response': get(root, 'request.script.res', ''),
|
||||
'docs': get(root, 'docs', '')
|
||||
};
|
||||
}, [activeItem]);
|
||||
}, [aiContext, collection?.draft?.root, collection?.root]);
|
||||
|
||||
const currentContent = allContent[contentType] || '';
|
||||
|
||||
// requestContext (URL/method/headers/response shape) only makes sense for
|
||||
// HTTP-style request items. Folder, collection, and App chats skip it —
|
||||
// App items live under kind: 'request' but have no URL/method to surface.
|
||||
const requestContext = useMemo(() => {
|
||||
if (!activeItem) return null;
|
||||
const draft = activeItem.draft;
|
||||
if (aiContext?.kind !== 'request' || !isItemARequest(aiContext.item)) return null;
|
||||
const item = aiContext.item;
|
||||
const draft = item.draft;
|
||||
return {
|
||||
url: draft ? get(activeItem, 'draft.request.url', '') : get(activeItem, 'request.url', ''),
|
||||
method: draft ? get(activeItem, 'draft.request.method', '') : get(activeItem, 'request.method', ''),
|
||||
headers: draft ? get(activeItem, 'draft.request.headers', []) : get(activeItem, 'request.headers', []),
|
||||
params: draft ? get(activeItem, 'draft.request.params', []) : get(activeItem, 'request.params', []),
|
||||
body: draft ? get(activeItem, 'draft.request.body', null) : get(activeItem, 'request.body', null),
|
||||
docs: draft ? get(activeItem, 'draft.request.docs', null) : get(activeItem, 'request.docs', null),
|
||||
responseStatus: get(activeItem, 'response.status', null),
|
||||
responseData: get(activeItem, 'response.data', null)
|
||||
url: draft ? get(item, 'draft.request.url', '') : get(item, 'request.url', ''),
|
||||
method: draft ? get(item, 'draft.request.method', '') : get(item, 'request.method', ''),
|
||||
headers: draft ? get(item, 'draft.request.headers', []) : get(item, 'request.headers', []),
|
||||
params: draft ? get(item, 'draft.request.params', []) : get(item, 'request.params', []),
|
||||
body: draft ? get(item, 'draft.request.body', null) : get(item, 'request.body', null),
|
||||
docs: draft ? get(item, 'draft.request.docs', null) : get(item, 'request.docs', null),
|
||||
responseStatus: get(item, 'response.status', null),
|
||||
responseData: get(item, 'response.data', null)
|
||||
};
|
||||
}, [activeItem]);
|
||||
}, [aiContext]);
|
||||
|
||||
const chatsWithMessages = useMemo(() => {
|
||||
if (!collection) return [];
|
||||
return Object.entries(allChats)
|
||||
.filter(([, chat]) => chat.messages?.length > 0)
|
||||
.map(([tabUid, chat]) => {
|
||||
if (tabUid === collection.uid) {
|
||||
return { id: tabUid, name: collection.name || 'Untitled Collection', method: 'ROOT', messageCount: chat.messages.length };
|
||||
}
|
||||
const item = findItemInCollection(collection, tabUid);
|
||||
if (!item) return null;
|
||||
if (isItemAFolder(item)) {
|
||||
return { id: tabUid, name: item.name || 'Untitled', method: 'FOLDER', messageCount: chat.messages.length };
|
||||
}
|
||||
const method = item.draft
|
||||
? get(item, 'draft.request.method', 'GET')
|
||||
: get(item, 'request.method', 'GET');
|
||||
@@ -375,7 +460,7 @@ const AiChatSidebar = ({ collection }) => {
|
||||
};
|
||||
|
||||
const handleApplyCode = (code, originalCode, messageIndex, msgContentType, writeIndex) => {
|
||||
if (!activeItem || code == null) return;
|
||||
if (!aiContext || code == null) return;
|
||||
const targetType = msgContentType || contentType;
|
||||
|
||||
// Bail if the live buffer has drifted from what the AI based the diff on.
|
||||
@@ -386,24 +471,35 @@ const AiChatSidebar = ({ collection }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = { itemUid: activeItem.uid, collectionUid: collection.uid };
|
||||
|
||||
switch (targetType) {
|
||||
case 'tests':
|
||||
dispatch(updateRequestTests({ ...payload, tests: code }));
|
||||
break;
|
||||
case 'pre-request':
|
||||
dispatch(updateRequestScript({ ...payload, script: code }));
|
||||
break;
|
||||
case 'post-response':
|
||||
dispatch(updateResponseScript({ ...payload, script: code }));
|
||||
break;
|
||||
case 'docs':
|
||||
dispatch(updateRequestDocs({ ...payload, docs: code }));
|
||||
break;
|
||||
default:
|
||||
dispatch(updateAppCode({ ...payload, code }));
|
||||
break;
|
||||
if (aiContext.kind === 'request') {
|
||||
const payload = { itemUid: aiContext.item.uid, collectionUid: collection.uid };
|
||||
switch (targetType) {
|
||||
case 'tests': dispatch(updateRequestTests({ ...payload, tests: code })); break;
|
||||
case 'pre-request': dispatch(updateRequestScript({ ...payload, script: code })); break;
|
||||
case 'post-response': dispatch(updateResponseScript({ ...payload, script: code })); break;
|
||||
case 'docs': dispatch(updateRequestDocs({ ...payload, docs: code })); break;
|
||||
default: dispatch(updateAppCode({ ...payload, code })); break;
|
||||
}
|
||||
} else if (aiContext.kind === 'folder') {
|
||||
const payload = { folderUid: aiContext.folder.uid, collectionUid: collection.uid };
|
||||
switch (targetType) {
|
||||
case 'tests': dispatch(updateFolderTests({ ...payload, tests: code })); break;
|
||||
case 'pre-request': dispatch(updateFolderRequestScript({ ...payload, script: code })); break;
|
||||
case 'post-response': dispatch(updateFolderResponseScript({ ...payload, script: code })); break;
|
||||
case 'docs': dispatch(updateFolderDocs({ ...payload, docs: code })); break;
|
||||
// Folders / collections have no 'app' equivalent. Bail rather than
|
||||
// marking the diff accepted when nothing was dispatched.
|
||||
default: return;
|
||||
}
|
||||
} else {
|
||||
const payload = { collectionUid: collection.uid };
|
||||
switch (targetType) {
|
||||
case 'tests': dispatch(updateCollectionTests({ ...payload, tests: code })); break;
|
||||
case 'pre-request': dispatch(updateCollectionRequestScript({ ...payload, script: code })); break;
|
||||
case 'post-response': dispatch(updateCollectionResponseScript({ ...payload, script: code })); break;
|
||||
case 'docs': dispatch(updateCollectionDocs({ ...payload, docs: code })); break;
|
||||
default: return;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(setMessageCodeStatus({
|
||||
@@ -630,7 +726,7 @@ const AiChatSidebar = ({ collection }) => {
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
if (!activeItem) return null;
|
||||
if (!aiContext) return null;
|
||||
|
||||
const placeholders = PLACEHOLDER_BY_TYPE[contentType] || PLACEHOLDER_BY_TYPE.app;
|
||||
const placeholder = currentContent ? placeholders.filled : placeholders.empty;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { IconApps } from '@tabler/icons';
|
||||
import { IconAppWindow } from '@tabler/icons';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
flex: 1 1 0;
|
||||
@@ -38,7 +38,7 @@ const Wrapper = styled.div`
|
||||
const EmptyAppState = ({ title = 'No app yet', hint }) => (
|
||||
<Wrapper data-testid="empty-app-state">
|
||||
<div className="empty-app-inner">
|
||||
<IconApps size={32} strokeWidth={1.25} />
|
||||
<IconAppWindow size={32} strokeWidth={1.25} />
|
||||
<div className="empty-app-title">{title}</div>
|
||||
{hint ? <div className="empty-app-hint">{hint}</div> : null}
|
||||
</div>
|
||||
|
||||
@@ -217,39 +217,35 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.mode-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
padding: 2px;
|
||||
gap: 2px;
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
overflow: hidden;
|
||||
|
||||
.mode-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 100%;
|
||||
width: 26px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-right: 1px solid ${(props) => props.theme.input.border};
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
||||
|
||||
&:hover:not(.active) {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
color: ${(props) => props.theme.primary.text};
|
||||
background: ${(props) => props.theme.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
border-color: ${(props) => props.theme.input.border};
|
||||
box-shadow: ${(props) => props.theme.shadow.sm};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
IconFileCode,
|
||||
IconFileOff,
|
||||
IconCode,
|
||||
IconApps,
|
||||
IconAppWindow,
|
||||
IconTransform,
|
||||
IconStars
|
||||
} from '@tabler/icons';
|
||||
@@ -651,61 +651,65 @@ const CollectionHeader = ({ collection, isScratchCollection }) => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-grow gap-1.5 items-center justify-end">
|
||||
{isAiEnabled && (
|
||||
<ToolHint text="AI Assistant" toolhintId="AiAssistantToolhintId" place="bottom">
|
||||
<ActionIcon
|
||||
onClick={() => dispatch(toggleAiSidebar())}
|
||||
aria-label="AI Assistant"
|
||||
size="sm"
|
||||
data-testid="ai-assistant"
|
||||
className={isAiSidebarOpen ? 'active' : ''}
|
||||
>
|
||||
<IconStars size={16} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
</ToolHint>
|
||||
)}
|
||||
{!isScratchCollection && (
|
||||
<>
|
||||
{isHttpRequestActive && (
|
||||
<ToolHint text="Switch view mode" toolhintId="ViewModeToggleToolhintId" place="bottom">
|
||||
<div className="mode-toggle" data-testid="view-mode-toggle">
|
||||
<div className="mode-toggle" data-testid="view-mode-toggle">
|
||||
<ToolHint text="Request" toolhintId="ViewModeRequestToolhintId" place="bottom">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="view-mode-request"
|
||||
aria-label="Request view"
|
||||
className={`mode-btn ${!appEnabled && !collection.fileMode ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
if (collection.fileMode) handleFileModeClick();
|
||||
if (appEnabled) handleToggleAppMode(false);
|
||||
}}
|
||||
title="Request"
|
||||
>
|
||||
<IconCode size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</ToolHint>
|
||||
<ToolHint text="App" toolhintId="ViewModeAppToolhintId" place="bottom">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="view-mode-app"
|
||||
aria-label="App view"
|
||||
className={`mode-btn ${appEnabled && !collection.fileMode ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
if (collection.fileMode) handleFileModeClick();
|
||||
if (!appEnabled) handleToggleAppMode(true);
|
||||
}}
|
||||
title="App"
|
||||
>
|
||||
<IconApps size={16} strokeWidth={1.5} />
|
||||
<IconAppWindow size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</ToolHint>
|
||||
<ToolHint text="File" toolhintId="ViewModeFileToolhintId" place="bottom">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="view-mode-file"
|
||||
aria-label="File view"
|
||||
className={`mode-btn ${collection.fileMode ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
if (appEnabled) handleToggleAppMode(false);
|
||||
if (!collection.fileMode) handleFileModeClick();
|
||||
}}
|
||||
title="File"
|
||||
>
|
||||
<IconFileCode size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
</ToolHint>
|
||||
</div>
|
||||
)}
|
||||
{isAiEnabled && (
|
||||
<ToolHint text="AI Assistant" toolhintId="AiAssistantToolhintId" place="bottom">
|
||||
<ActionIcon
|
||||
onClick={() => dispatch(toggleAiSidebar())}
|
||||
aria-label="AI Assistant"
|
||||
size="sm"
|
||||
data-testid="ai-assistant"
|
||||
className={isAiSidebarOpen ? 'active' : ''}
|
||||
>
|
||||
<IconStars size={16} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
</ToolHint>
|
||||
)}
|
||||
{collection.format === 'bru' && !migratePillDismissed && (
|
||||
|
||||
@@ -16,7 +16,7 @@ import ConfirmCloseEnvironment from 'components/Environments/ConfirmCloseEnviron
|
||||
import RequestTabNotFound from './RequestTabNotFound';
|
||||
import RequestTabLoading from './RequestTabLoading';
|
||||
import SpecialTab from './SpecialTab';
|
||||
import { IconApps } from '@tabler/icons';
|
||||
import { IconAppWindow } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import CloneCollectionItem from 'components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index';
|
||||
@@ -583,7 +583,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
>
|
||||
{item.type === 'app' ? (
|
||||
<span className="tab-method flex items-center" aria-label="App">
|
||||
<IconApps size={14} strokeWidth={1.5} />
|
||||
<IconAppWindow size={14} strokeWidth={1.5} />
|
||||
</span>
|
||||
) : (
|
||||
<span className="tab-method uppercase" style={{ color: getMethodColor(method) }}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import RequestMethod from '../RequestMethod';
|
||||
import { IconLoader2, IconAlertTriangle, IconAlertCircle, IconApps } from '@tabler/icons';
|
||||
import { IconLoader2, IconAlertTriangle, IconAlertCircle, IconAppWindow } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CollectionItemIcon = ({ item }) => {
|
||||
@@ -16,7 +16,7 @@ const CollectionItemIcon = ({ item }) => {
|
||||
}
|
||||
|
||||
if (item?.type === 'app') {
|
||||
return <IconApps className="w-fit mr-2" size={16} strokeWidth={1.5} />;
|
||||
return <IconAppWindow className="w-fit mr-2" size={16} strokeWidth={1.5} />;
|
||||
}
|
||||
|
||||
return <RequestMethod item={item} />;
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
IconSettings,
|
||||
IconInfoCircle,
|
||||
IconTerminal2,
|
||||
IconApps
|
||||
IconAppWindow
|
||||
} from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { addTab, focusTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
|
||||
@@ -357,7 +357,7 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
},
|
||||
{
|
||||
id: 'new-app',
|
||||
leftSection: IconApps,
|
||||
leftSection: IconAppWindow,
|
||||
label: 'New App',
|
||||
onClick: () => setNewAppModalOpen(true)
|
||||
},
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
IconFolder,
|
||||
IconBook,
|
||||
IconFileArrowRight,
|
||||
IconApps
|
||||
IconAppWindow
|
||||
} from '@tabler/icons';
|
||||
import OpenAPISyncIcon from 'components/Icons/OpenAPISync';
|
||||
import { toggleCollection, collapseFullCollection } from 'providers/ReduxStore/slices/collections';
|
||||
@@ -368,7 +368,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
},
|
||||
{
|
||||
id: 'new-app',
|
||||
leftSection: IconApps,
|
||||
leftSection: IconAppWindow,
|
||||
label: 'New App',
|
||||
onClick: () => {
|
||||
ensureCollectionIsMounted();
|
||||
|
||||
Reference in New Issue
Block a user