Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import styled from 'styled-components';

const StyledWrapper = styled.div`
.editing-mode {
cursor: pointer;
color: ${(props) => props.theme.colors.text.yellow};
}
`;

export default StyledWrapper;
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import 'github-markdown-css/github-markdown.css';
import get from 'lodash/get';
import { updateFolderDocs } from 'providers/ReduxStore/slices/collections';
import { useTheme } from 'providers/Theme';
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import Markdown from 'components/MarkDown';
import CodeEditor from 'components/CodeEditor';
import StyledWrapper from './StyledWrapper';
import { IconEdit, IconX, IconFileText } from '@tabler/icons';

const Docs = ({ collection, folder }) => {
const dispatch = useDispatch();
const { displayedTheme } = useTheme();
const [isEditing, setIsEditing] = useState(false);
const rootDocs = get(folder, 'root.docs', '');
const docs = folder.draft ? get(folder, 'draft.docs', rootDocs) : rootDocs;
const preferences = useSelector((state) => state.app.preferences);

const toggleViewMode = () => {
setIsEditing((prev) => !prev);
};

const onEdit = (value) => {
dispatch(updateFolderDocs({
folderUid: folder.uid,
collectionUid: collection.uid,
docs: value
}));
};

const handleDiscardChanges = () => {
dispatch(updateFolderDocs({
folderUid: folder.uid,
collectionUid: collection.uid,
docs: rootDocs
}));
toggleViewMode();
};

const onSave = () => {
dispatch(saveFolderRoot(collection.uid, folder.uid));
toggleViewMode();
};

return (
<StyledWrapper className="h-full w-full relative flex flex-col">
<div className="flex flex-row w-full justify-between items-center mb-4">
<div className="text-lg font-medium flex items-center gap-2">
<IconFileText size={20} strokeWidth={1.5} />
Documentation
</div>
<div className="flex flex-row gap-2 items-center justify-center">
{isEditing ? (
<>
<div className="editing-mode" role="tab" onClick={handleDiscardChanges}>
<IconX className="cursor-pointer" size={20} strokeWidth={1.5} />
</div>
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={onSave}>
Save
</button>
</>
) : (
<div className="editing-mode" role="tab" onClick={toggleViewMode}>
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using role="tab" on a div that functions as an edit button is semantically incorrect. This element is a button for toggling edit mode, not a tab. Consider removing the role attribute or changing it to role="button" with appropriate aria-label for better accessibility.

Copilot uses AI. Check for mistakes.
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} />
</div>
Comment on lines +57 to +67
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using role="tab" on a button element is semantically incorrect. This element is a button for discarding changes, not a tab. Consider removing the role attribute or changing it to role="button" with appropriate aria-label for better accessibility.

Suggested change
<div className="editing-mode" role="tab" onClick={handleDiscardChanges}>
<IconX className="cursor-pointer" size={20} strokeWidth={1.5} />
</div>
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={onSave}>
Save
</button>
</>
) : (
<div className="editing-mode" role="tab" onClick={toggleViewMode}>
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} />
</div>
<button
className="editing-mode"
type="button"
aria-label="Discard changes"
onClick={handleDiscardChanges}
>
<IconX className="cursor-pointer" size={20} strokeWidth={1.5} />
</button>
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={onSave}>
Save
</button>
</>
) : (
<button
className="editing-mode"
type="button"
aria-label="Edit documentation"
onClick={toggleViewMode}
>
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} />
</button>

Copilot uses AI. Check for mistakes.
)}
</div>
</div>
{isEditing ? (
<CodeEditor
collection={collection}
theme={displayedTheme}
value={docs}
onEdit={onEdit}
onSave={onSave}
mode="application/text"
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/>
) : (
<div className="h-full overflow-auto pl-1">
<div className="h-[1px] min-h-[500px]">
{
docs?.length > 0
? <Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
: <Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={documentationPlaceholder} />
}
</div>
</div>
)}
</StyledWrapper>
);
};

export default Docs;

const documentationPlaceholder = `
Welcome to your folder documentation! This space is designed to help you document this folder's API requests.

## Overview
Use this section to provide a high-level overview of this folder. You can describe:
- The purpose of these API endpoints
- Key features and functionalities
- Folder-specific configurations

## Best Practices
- Keep documentation up to date
- Include request/response examples
- Document error scenarios
- Add relevant links and references

## Markdown Support
This documentation supports Markdown formatting! You can use:
- **Bold** and *italic* text
- \`code blocks\` and syntax highlighting
- Tables and lists
- [Links](https://usebruno.com)
- And more!
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react';
import { getTotalRequestCountInCollection, getTreePathFromCollectionToItem, areItemsLoading, getItemsLoadStats } from 'utils/collections/';
import { IconFolder, IconApi, IconSubtask } from '@tabler/icons';

const Info = ({ collection, folder }) => {
const totalRequestsInFolder = getTotalRequestCountInCollection(folder);
const isFolderLoading = areItemsLoading(folder);
const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(folder);

// Count direct child folders
const childFoldersCount = (folder.items || []).filter((item) => item.type === 'folder').length;

// Get relative path from collection to folder
const treePath = getTreePathFromCollectionToItem(collection, folder);
const relativePath = treePath.map((item) => item.name).join(' / ');

return (
<div className="w-full flex flex-col h-fit">
<div className="rounded-lg py-6">
<div className="grid gap-5">
{/* Location Row */}
<div className="flex items-start">
<div className="flex-shrink-0 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
<IconFolder className="w-5 h-5 text-green-500" stroke={1.5} />
</div>
<div className="ml-4">
<div className="font-medium">Location</div>
<div className="mt-1 text-muted break-all text-xs">
{relativePath}
</div>
</div>
</div>

{/* Requests Row */}
<div className="flex items-start">
<div className="flex-shrink-0 p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<IconApi className="w-5 h-5 text-purple-500" stroke={1.5} />
</div>
<div className="ml-4">
<div className="font-medium">Requests</div>
<div className="mt-1 text-muted text-xs">
{
isFolderLoading ? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the folder loaded` : `${totalRequestsInFolder} request${totalRequestsInFolder !== 1 ? 's' : ''} in folder`
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message "requests in the folder loaded" is grammatically incorrect. It should be "requests in the folder have been loaded" or simply "requests loaded".

Suggested change
isFolderLoading ? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the folder loaded` : `${totalRequestsInFolder} request${totalRequestsInFolder !== 1 ? 's' : ''} in folder`
isFolderLoading ? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the folder have been loaded` : `${totalRequestsInFolder} request${totalRequestsInFolder !== 1 ? 's' : ''} in folder`

Copilot uses AI. Check for mistakes.
}
</div>
</div>
</div>

{/* Subfolders Row */}
<div className="flex items-start">
<div className="flex-shrink-0 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<IconSubtask className="w-5 h-5 text-blue-500" stroke={1.5} />
</div>
<div className="ml-4">
<div className="font-medium">Subfolders</div>
<div className="mt-1 text-muted text-xs">
{childFoldersCount} subfolder{childFoldersCount !== 1 ? 's' : ''}
</div>
</div>
</div>
</div>
</div>
</div>
);
};

export default Info;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Docs from './Docs';
import Info from './Info';
import { IconFolder } from '@tabler/icons';

const Overview = ({ collection, folder }) => {
return (
<div className="h-full">
<div className="grid grid-cols-5 gap-5 h-full">
<div className="col-span-2">
<div className="text-lg font-medium flex items-center gap-2">
<IconFolder size={20} stroke={1.5} />
{folder?.name}
</div>
<Info collection={collection} folder={folder} />
</div>
<div className="col-span-3">
<Docs collection={collection} folder={folder} />
</div>
</div>
</div>
);
};

export default Overview;
16 changes: 8 additions & 8 deletions packages/bruno-app/src/components/FolderSettings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import Script from './Script';
import Tests from './Tests';
import StyledWrapper from './StyledWrapper';
import Vars from './Vars';
import Documentation from './Documentation';
import Auth from './Auth';
import Overview from './Overview';
import StatusDot from 'components/StatusDot';
import get from 'lodash/get';

const FolderSettings = ({ collection, folder }) => {
const dispatch = useDispatch();
let tab = 'headers';
let tab = 'overview';
const { folderLevelSettingsSelectedTab } = collection;
if (folderLevelSettingsSelectedTab?.[folder?.uid]) {
tab = folderLevelSettingsSelectedTab[folder?.uid];
Expand Down Expand Up @@ -46,6 +46,9 @@ const FolderSettings = ({ collection, folder }) => {

const getTabPanel = (tab) => {
switch (tab) {
case 'overview': {
return <Overview collection={collection} folder={folder} />;
}
case 'headers': {
return <Headers collection={collection} folder={folder} />;
}
Expand All @@ -61,9 +64,6 @@ const FolderSettings = ({ collection, folder }) => {
case 'auth': {
return <Auth collection={collection} folder={folder} />;
}
case 'docs': {
return <Documentation collection={collection} folder={folder} />;
}
}
};

Expand All @@ -77,6 +77,9 @@ const FolderSettings = ({ collection, folder }) => {
<StyledWrapper className="flex flex-col h-full overflow-auto">
<div className="flex flex-col h-full relative px-4 py-4">
<div className="flex flex-wrap items-center tabs" role="tablist">
<div className={getTabClassname('overview')} role="tab" onClick={() => setTab('overview')}>
Overview
</div>
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
Headers
{activeHeadersCount > 0 && <sup className="ml-1 font-medium">{activeHeadersCount}</sup>}
Expand All @@ -97,9 +100,6 @@ const FolderSettings = ({ collection, folder }) => {
Auth
{hasAuth && <StatusDot />}
</div>
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
Docs
</div>
</div>
<section className={`flex mt-4 h-full overflow-auto`}>{getTabPanel(tab)}</section>
</div>
Expand Down
5 changes: 4 additions & 1 deletion tests/collection/draft/draft-indicator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,10 @@ test.describe('Draft indicator in collection and folder settings', () => {
await expect(folderTab.locator('.close-icon')).toBeVisible();
await expect(folderTab.locator('.has-changes-icon')).not.toBeVisible();

// Headers tab should be selected by default, add a new header
// Click on Headers tab
await page.locator('.tab.headers').click();

// Add a new header
await page.getByRole('button', { name: 'Add Header' }).click();

// Fill in header name and value in the table
Expand Down
Loading