mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
feat: inline create collection and workspace editor (#7324)
* feat: inline create collection and workspace editor * refactor: use inline collection creation from workspace overview * fix: improve inline collection creation UX from workspace overview * fix: update E2E tests for inline collection creation flow * fix: update default location test for inline collection creation flow * fix: derive inline workspace/collection names from filesystem * feat: inline workspace create form manage workspace * feat: prefill create modal with name * fix: minor code style fixes --------- Co-authored-by: naman-bruno <naman@usebruno.com>
This commit is contained in:
@@ -6,9 +6,10 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||||||
|
|
||||||
import { savePreferences, showManageWorkspacePage, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
import { savePreferences, showManageWorkspacePage, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
||||||
import { closeConsole, openConsole } from 'providers/ReduxStore/slices/logs';
|
import { closeConsole, openConsole } from 'providers/ReduxStore/slices/logs';
|
||||||
import { openWorkspaceDialog, switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
import { createWorkspaceWithUniqueName, openWorkspaceDialog, switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||||
import { sortWorkspaces, toggleWorkspacePin } from 'utils/workspaces';
|
import { sortWorkspaces, toggleWorkspacePin } from 'utils/workspaces';
|
||||||
import { focusTab } from 'providers/ReduxStore/slices/tabs';
|
import { focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
|
||||||
import Bruno from 'components/Bruno';
|
import Bruno from 'components/Bruno';
|
||||||
import MenuDropdown from 'ui/MenuDropdown';
|
import MenuDropdown from 'ui/MenuDropdown';
|
||||||
@@ -150,9 +151,20 @@ const AppTitleBar = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateWorkspace = () => {
|
const handleCreateWorkspace = useCallback(async () => {
|
||||||
setCreateWorkspaceModalOpen(true);
|
const defaultLocation = get(preferences, 'general.defaultLocation', '');
|
||||||
};
|
if (!defaultLocation) {
|
||||||
|
setCreateWorkspaceModalOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dispatch(createWorkspaceWithUniqueName(defaultLocation));
|
||||||
|
toast.success('Workspace created!');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error?.message || 'Failed to create workspace');
|
||||||
|
}
|
||||||
|
}, [preferences, dispatch]);
|
||||||
|
|
||||||
const handleManageWorkspaces = () => {
|
const handleManageWorkspaces = () => {
|
||||||
dispatch(showManageWorkspacePage());
|
dispatch(showManageWorkspacePage());
|
||||||
@@ -240,7 +252,7 @@ const AppTitleBar = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}, [sortedWorkspaces, activeWorkspaceUid, preferences, handlePinWorkspace]);
|
}, [sortedWorkspaces, activeWorkspaceUid, preferences, handlePinWorkspace, handleCreateWorkspace]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className={`app-titlebar ${osClass} ${isFullScreen ? 'fullscreen' : ''}`}>
|
<StyledWrapper className={`app-titlebar ${osClass} ${isFullScreen ? 'fullscreen' : ''}`}>
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { useSelector, useDispatch } from 'react-redux';
|
|||||||
import { IconArrowLeft, IconPlus, IconFolder, IconLock, IconDots, IconCategory, IconLogin } from '@tabler/icons';
|
import { IconArrowLeft, IconPlus, IconFolder, IconLock, IconDots, IconCategory, IconLogin } from '@tabler/icons';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
import get from 'lodash/get';
|
||||||
import { showHomePage } from 'providers/ReduxStore/slices/app';
|
import { showHomePage } from 'providers/ReduxStore/slices/app';
|
||||||
import { switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
import { createWorkspaceWithUniqueName, switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||||
import { showInFolder } from 'providers/ReduxStore/slices/collections/actions';
|
import { showInFolder } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
import { sortWorkspaces } from 'utils/workspaces';
|
import { sortWorkspaces } from 'utils/workspaces';
|
||||||
|
|
||||||
@@ -59,6 +60,21 @@ const ManageWorkspace = () => {
|
|||||||
setDeleteWorkspaceModal({ open: true, workspace });
|
setDeleteWorkspaceModal({ open: true, workspace });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCreateWorkspace = async () => {
|
||||||
|
const defaultLocation = get(preferences, 'general.defaultLocation', '');
|
||||||
|
if (!defaultLocation) {
|
||||||
|
setCreateWorkspaceModalOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dispatch(createWorkspaceWithUniqueName(defaultLocation));
|
||||||
|
toast.success('Workspace created!');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error?.message || 'Failed to create workspace');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
{createWorkspaceModalOpen && (
|
{createWorkspaceModalOpen && (
|
||||||
@@ -86,7 +102,7 @@ const ManageWorkspace = () => {
|
|||||||
</div>
|
</div>
|
||||||
<span className="header-title">Manage Workspace</span>
|
<span className="header-title">Manage Workspace</span>
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" onClick={() => setCreateWorkspaceModalOpen(true)} icon={<IconPlus size={14} strokeWidth={2} />}>
|
<Button size="sm" onClick={handleCreateWorkspace} icon={<IconPlus size={14} strokeWidth={2} />}>
|
||||||
Create Workspace
|
Create Workspace
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
IconUpload
|
IconUpload
|
||||||
} from '@tabler/icons';
|
} from '@tabler/icons';
|
||||||
import { switchWorkspace, renameWorkspaceAction, exportWorkspaceAction } from 'providers/ReduxStore/slices/workspaces/actions';
|
import { switchWorkspace, renameWorkspaceAction, exportWorkspaceAction } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||||
|
import { updateWorkspace } from 'providers/ReduxStore/slices/workspaces';
|
||||||
import { showInFolder } from 'providers/ReduxStore/slices/collections/actions';
|
import { showInFolder } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
|
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||||
import { uuid } from 'utils/common';
|
import { uuid } from 'utils/common';
|
||||||
@@ -53,6 +54,21 @@ const CollectionHeader = ({ collection, isScratchCollection }) => {
|
|||||||
const onSwitcherCreate = (ref) => (switcherRef.current = ref);
|
const onSwitcherCreate = (ref) => (switcherRef.current = ref);
|
||||||
const onWorkspaceActionsCreate = (ref) => (workspaceActionsRef.current = ref);
|
const onWorkspaceActionsCreate = (ref) => (workspaceActionsRef.current = ref);
|
||||||
|
|
||||||
|
// Auto-enter rename mode when workspace is newly created
|
||||||
|
useEffect(() => {
|
||||||
|
if (isScratchCollection && currentWorkspace?.isNewlyCreated) {
|
||||||
|
dispatch(updateWorkspace({ uid: currentWorkspace.uid, isNewlyCreated: false }));
|
||||||
|
setIsRenamingWorkspace(true);
|
||||||
|
setWorkspaceNameInput(currentWorkspace.name || '');
|
||||||
|
setWorkspaceNameError('');
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
workspaceNameInputRef.current?.focus();
|
||||||
|
workspaceNameInputRef.current?.select();
|
||||||
|
}, 50);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [isScratchCollection, currentWorkspace?.isNewlyCreated, currentWorkspace?.uid, currentWorkspace?.name, dispatch]);
|
||||||
|
|
||||||
const handleCancelWorkspaceRename = useCallback(() => {
|
const handleCancelWorkspaceRename = useCallback(() => {
|
||||||
setIsRenamingWorkspace(false);
|
setIsRenamingWorkspace(false);
|
||||||
setWorkspaceNameInput('');
|
setWorkspaceNameInput('');
|
||||||
|
|||||||
@@ -1,24 +1,18 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { useTheme } from '../../../../providers/Theme';
|
import { useTheme } from '../../../../providers/Theme';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { openCollection } from 'providers/ReduxStore/slices/collections/actions';
|
import { openCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
|
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import CreateCollection from 'components/Sidebar/CreateCollection';
|
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
const LinkStyle = styled.span`
|
const LinkStyle = styled.span`
|
||||||
color: ${(props) => props.theme['text-link']};
|
color: ${(props) => props.theme['text-link']};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CreateOrOpenCollection = () => {
|
const CreateOrOpenCollection = ({ onCreateClick }) => {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
|
||||||
|
|
||||||
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
|
||||||
const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);
|
|
||||||
|
|
||||||
const handleOpenCollection = () => {
|
const handleOpenCollection = () => {
|
||||||
dispatch(openCollection()).catch(
|
dispatch(openCollection()).catch(
|
||||||
@@ -32,7 +26,7 @@ const CreateOrOpenCollection = () => {
|
|||||||
<LinkStyle
|
<LinkStyle
|
||||||
className="underline text-link cursor-pointer"
|
className="underline text-link cursor-pointer"
|
||||||
theme={theme}
|
theme={theme}
|
||||||
onClick={() => setCreateCollectionModalOpen(true)}
|
onClick={onCreateClick}
|
||||||
>
|
>
|
||||||
Create
|
Create
|
||||||
</LinkStyle>
|
</LinkStyle>
|
||||||
@@ -45,12 +39,6 @@ const CreateOrOpenCollection = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className="px-2 mt-4">
|
<StyledWrapper className="px-2 mt-4">
|
||||||
{createCollectionModalOpen ? (
|
|
||||||
<CreateCollection
|
|
||||||
onClose={() => setCreateCollectionModalOpen(false)}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="text-xs text-center">
|
<div className="text-xs text-center">
|
||||||
<div>No collections found.</div>
|
<div>No collections found.</div>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const StyledWrapper = styled.div`
|
||||||
|
.inline-collection-creator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
height: 1.6rem;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid ${(props) => props.theme.input.border};
|
||||||
|
border-radius: 3px;
|
||||||
|
background: ${(props) => props.theme.input.bg};
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: ${(props) => props.theme.input.focusBorder};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-collection-input {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 1px 4px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: ${(props) => props.theme.text};
|
||||||
|
outline: none;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cog-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 20px;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
color: ${(props) => props.theme.text};
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-action-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
color: ${(props) => props.theme.text};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||||
|
}
|
||||||
|
|
||||||
|
&.save {
|
||||||
|
color: ${(props) => props.theme.colors.text.green};
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cancel {
|
||||||
|
color: ${(props) => props.theme.colors.text.danger};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default StyledWrapper;
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import { useRef, useEffect, useState, useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { IconCheck, IconX, IconSettings } from '@tabler/icons';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { createCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
|
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
|
||||||
|
import { DEFAULT_COLLECTION_FORMAT } from 'utils/common/constants';
|
||||||
|
import { multiLineMsg } from 'utils/common';
|
||||||
|
import { formatIpcError } from 'utils/common/error';
|
||||||
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
|
const InlineCollectionCreator = ({ onComplete, onCancel, onOpenAdvanced }) => {
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
|
const openingAdvancedRef = useRef(false);
|
||||||
|
const clickedOutsideRef = useRef(false);
|
||||||
|
|
||||||
|
const preferences = useSelector((state) => state.app.preferences);
|
||||||
|
const workspaces = useSelector((state) => state.workspaces?.workspaces || []);
|
||||||
|
const activeWorkspaceUid = useSelector((state) => state.workspaces?.activeWorkspaceUid);
|
||||||
|
const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);
|
||||||
|
const isDefaultWorkspace = activeWorkspace?.type === 'default';
|
||||||
|
|
||||||
|
const defaultLocation = isDefaultWorkspace
|
||||||
|
? get(preferences, 'general.defaultLocation', '')
|
||||||
|
: (activeWorkspace?.pathname ? `${activeWorkspace.pathname}/collections` : '');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const focusAndSelect = (value) => {
|
||||||
|
if (!inputRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
inputRef.current.value = value;
|
||||||
|
}
|
||||||
|
inputRef.current.focus();
|
||||||
|
inputRef.current.select();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (defaultLocation) {
|
||||||
|
window.ipcRenderer?.invoke('renderer:find-unique-folder-name', 'untitled collection', defaultLocation)
|
||||||
|
?.then((name) => focusAndSelect(name))
|
||||||
|
?.catch(() => focusAndSelect());
|
||||||
|
} else {
|
||||||
|
focusAndSelect();
|
||||||
|
}
|
||||||
|
}, [defaultLocation]);
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
if (isCreating || openingAdvancedRef.current) return;
|
||||||
|
onCancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = useCallback(async () => {
|
||||||
|
const fromOutside = clickedOutsideRef.current;
|
||||||
|
clickedOutsideRef.current = false;
|
||||||
|
|
||||||
|
if (isCreating || openingAdvancedRef.current) return;
|
||||||
|
|
||||||
|
const name = inputRef.current?.value?.trim();
|
||||||
|
if (!name) {
|
||||||
|
if (fromOutside) {
|
||||||
|
onCancel();
|
||||||
|
} else {
|
||||||
|
toast.error('Collection name is required');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateName(name)) {
|
||||||
|
toast.error(validateNameError(name));
|
||||||
|
if (fromOutside) {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defaultLocation) {
|
||||||
|
toast.error('Please set a default location in Preferences > General');
|
||||||
|
onCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsCreating(true);
|
||||||
|
try {
|
||||||
|
const folderName = sanitizeName(name);
|
||||||
|
await dispatch(createCollection(name, folderName, defaultLocation, { format: DEFAULT_COLLECTION_FORMAT }));
|
||||||
|
toast.success('Collection created!');
|
||||||
|
onComplete();
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(multiLineMsg('An error occurred while creating the collection', formatIpcError(e)));
|
||||||
|
setIsCreating(false);
|
||||||
|
}
|
||||||
|
}, [isCreating, defaultLocation, dispatch, onCancel, onComplete]);
|
||||||
|
|
||||||
|
// Click outside to create
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e) => {
|
||||||
|
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
||||||
|
clickedOutsideRef.current = true;
|
||||||
|
handleCreate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, [handleCreate]);
|
||||||
|
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleCreate();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleCancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper>
|
||||||
|
<div className="inline-collection-creator" ref={containerRef}>
|
||||||
|
<div className="input-wrapper">
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
className="inline-collection-input"
|
||||||
|
defaultValue="untitled collection"
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
|
disabled={isCreating}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="cog-btn"
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
onClick={() => {
|
||||||
|
openingAdvancedRef.current = true;
|
||||||
|
onOpenAdvanced(inputRef.current?.value?.trim());
|
||||||
|
}}
|
||||||
|
title="Advanced options"
|
||||||
|
disabled={isCreating}
|
||||||
|
>
|
||||||
|
<IconSettings size={13} strokeWidth={1.5} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="inline-actions">
|
||||||
|
<button
|
||||||
|
className="inline-action-btn save"
|
||||||
|
onClick={handleCreate}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
title="Create"
|
||||||
|
disabled={isCreating}
|
||||||
|
>
|
||||||
|
<IconCheck size={14} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="inline-action-btn cancel"
|
||||||
|
onClick={handleCancel}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
title="Cancel"
|
||||||
|
disabled={isCreating}
|
||||||
|
>
|
||||||
|
<IconX size={14} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InlineCollectionCreator;
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import Collection from './Collection';
|
import Collection from './Collection';
|
||||||
import CreateCollection from '../CreateCollection';
|
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
import CreateOrOpenCollection from './CreateOrOpenCollection';
|
import CreateOrOpenCollection from './CreateOrOpenCollection';
|
||||||
import CollectionSearch from './CollectionSearch/index';
|
import CollectionSearch from './CollectionSearch/index';
|
||||||
|
import InlineCollectionCreator from './InlineCollectionCreator';
|
||||||
import { normalizePath } from 'utils/common/path';
|
import { normalizePath } from 'utils/common/path';
|
||||||
import { isScratchCollection } from 'utils/collections';
|
import { isScratchCollection } from 'utils/collections';
|
||||||
|
|
||||||
const Collections = ({ showSearch }) => {
|
const Collections = ({ showSearch, isCreatingCollection, onCreateClick, onDismissCreate, onOpenAdvancedCreate }) => {
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const { collections } = useSelector((state) => state.collections);
|
const { collections } = useSelector((state) => state.collections);
|
||||||
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
|
||||||
|
|
||||||
const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid) || workspaces.find((w) => w.type === 'default');
|
const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid) || workspaces.find((w) => w.type === 'default');
|
||||||
|
|
||||||
@@ -30,24 +29,32 @@ const Collections = ({ showSearch }) => {
|
|||||||
if (!workspaceCollections || !workspaceCollections.length) {
|
if (!workspaceCollections || !workspaceCollections.length) {
|
||||||
return (
|
return (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
<CreateOrOpenCollection />
|
{isCreatingCollection && (
|
||||||
|
<InlineCollectionCreator
|
||||||
|
onComplete={onDismissCreate}
|
||||||
|
onCancel={onDismissCreate}
|
||||||
|
onOpenAdvanced={onOpenAdvancedCreate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isCreatingCollection && <CreateOrOpenCollection onCreateClick={onCreateClick} />}
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper data-testid="collections">
|
<StyledWrapper data-testid="collections">
|
||||||
{createCollectionModalOpen ? (
|
|
||||||
<CreateCollection
|
|
||||||
onClose={() => setCreateCollectionModalOpen(false)}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{showSearch && (
|
{showSearch && (
|
||||||
<CollectionSearch searchText={searchText} setSearchText={setSearchText} />
|
<CollectionSearch searchText={searchText} setSearchText={setSearchText} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="collections-list">
|
<div className="collections-list">
|
||||||
|
{isCreatingCollection && (
|
||||||
|
<InlineCollectionCreator
|
||||||
|
onComplete={onDismissCreate}
|
||||||
|
onCancel={onDismissCreate}
|
||||||
|
onOpenAdvanced={onOpenAdvancedCreate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{workspaceCollections && workspaceCollections.length
|
{workspaceCollections && workspaceCollections.length
|
||||||
? workspaceCollections.map((c) => {
|
? workspaceCollections.map((c) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import StyledWrapper from './StyledWrapper';
|
|||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import Button from 'ui/Button';
|
import Button from 'ui/Button';
|
||||||
|
|
||||||
const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation }) => {
|
const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation, initialCollectionName = '' }) => {
|
||||||
const inputRef = useRef();
|
const inputRef = useRef();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const workspaces = useSelector((state) => state.workspaces?.workspaces || []);
|
const workspaces = useSelector((state) => state.workspaces?.workspaces || []);
|
||||||
@@ -37,8 +37,8 @@ const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation }) =>
|
|||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
collectionName: '',
|
collectionName: initialCollectionName,
|
||||||
collectionFolderName: '',
|
collectionFolderName: initialCollectionName ? sanitizeName(initialCollectionName) : '',
|
||||||
collectionLocation: defaultLocation || '',
|
collectionLocation: defaultLocation || '',
|
||||||
format: DEFAULT_COLLECTION_FORMAT
|
format: DEFAULT_COLLECTION_FORMAT
|
||||||
},
|
},
|
||||||
@@ -86,9 +86,13 @@ const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation }) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputRef && inputRef.current) {
|
const timer = setTimeout(() => {
|
||||||
inputRef.current.focus();
|
if (inputRef && inputRef.current) {
|
||||||
}
|
inputRef.current.focus();
|
||||||
|
inputRef.current.select();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
}, [inputRef]);
|
}, [inputRef]);
|
||||||
|
|
||||||
const AdvancedOptions = forwardRef((props, ref) => {
|
const AdvancedOptions = forwardRef((props, ref) => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useMemo, useEffect } from 'react';
|
import { useState, useMemo, useEffect } from 'react';
|
||||||
|
import { setIsCreatingCollection } from 'providers/ReduxStore/slices/app';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
@@ -46,11 +47,13 @@ const CollectionsSection = () => {
|
|||||||
|
|
||||||
const { collections } = useSelector((state) => state.collections);
|
const { collections } = useSelector((state) => state.collections);
|
||||||
const { collectionSortOrder } = useSelector((state) => state.collections);
|
const { collectionSortOrder } = useSelector((state) => state.collections);
|
||||||
|
const { isCreatingCollection } = useSelector((state) => state.app);
|
||||||
const preferences = useSelector((state) => state.app.preferences);
|
const preferences = useSelector((state) => state.app.preferences);
|
||||||
const [collectionsToClose, setCollectionsToClose] = useState([]);
|
const [collectionsToClose, setCollectionsToClose] = useState([]);
|
||||||
|
|
||||||
const [importData, setImportData] = useState(null);
|
const [importData, setImportData] = useState(null);
|
||||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||||
|
const [advancedCreateName, setAdvancedCreateName] = useState('');
|
||||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||||
const [showCloneGitModal, setShowCloneGitModal] = useState(false);
|
const [showCloneGitModal, setShowCloneGitModal] = useState(false);
|
||||||
@@ -241,13 +244,19 @@ const CollectionsSection = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenAdvancedCreate = (name) => {
|
||||||
|
dispatch(setIsCreatingCollection(false));
|
||||||
|
setAdvancedCreateName(name || '');
|
||||||
|
setCreateCollectionModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const addDropdownItems = [
|
const addDropdownItems = [
|
||||||
{
|
{
|
||||||
id: 'create',
|
id: 'create',
|
||||||
leftSection: IconPlus,
|
leftSection: IconPlus,
|
||||||
label: 'Create collection',
|
label: 'Create collection',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setCreateCollectionModalOpen(true);
|
dispatch(setIsCreatingCollection(true));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -359,7 +368,11 @@ const CollectionsSection = () => {
|
|||||||
)}
|
)}
|
||||||
{createCollectionModalOpen && (
|
{createCollectionModalOpen && (
|
||||||
<CreateCollection
|
<CreateCollection
|
||||||
onClose={() => setCreateCollectionModalOpen(false)}
|
onClose={() => {
|
||||||
|
setCreateCollectionModalOpen(false);
|
||||||
|
setAdvancedCreateName('');
|
||||||
|
}}
|
||||||
|
initialCollectionName={advancedCreateName}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{importCollectionModalOpen && (
|
{importCollectionModalOpen && (
|
||||||
@@ -396,7 +409,13 @@ const CollectionsSection = () => {
|
|||||||
icon={IconBox}
|
icon={IconBox}
|
||||||
actions={sectionActions}
|
actions={sectionActions}
|
||||||
>
|
>
|
||||||
<Collections showSearch={showSearch} />
|
<Collections
|
||||||
|
showSearch={showSearch}
|
||||||
|
isCreatingCollection={isCreatingCollection}
|
||||||
|
onCreateClick={() => dispatch(setIsCreatingCollection(true))}
|
||||||
|
onDismissCreate={() => dispatch(setIsCreatingCollection(false))}
|
||||||
|
onOpenAdvancedCreate={handleOpenAdvancedCreate}
|
||||||
|
/>
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import React, { useState } from 'react';
|
|||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { IconPlus, IconFolder, IconDownload } from '@tabler/icons';
|
import { IconPlus, IconFolder, IconDownload } from '@tabler/icons';
|
||||||
import { importCollection, openCollection, importCollectionFromZip } from 'providers/ReduxStore/slices/collections/actions';
|
import { importCollection, openCollection, importCollectionFromZip } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
|
import { setIsCreatingCollection, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import CreateCollection from 'components/Sidebar/CreateCollection';
|
|
||||||
import ImportCollection from 'components/Sidebar/ImportCollection';
|
import ImportCollection from 'components/Sidebar/ImportCollection';
|
||||||
import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation';
|
import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation';
|
||||||
import BulkImportCollectionLocation from 'components/Sidebar/BulkImportCollectionLocation';
|
import BulkImportCollectionLocation from 'components/Sidebar/BulkImportCollectionLocation';
|
||||||
@@ -16,8 +16,8 @@ import StyledWrapper from './StyledWrapper';
|
|||||||
const WorkspaceOverview = ({ workspace }) => {
|
const WorkspaceOverview = ({ workspace }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { globalEnvironments } = useSelector((state) => state.globalEnvironments);
|
const { globalEnvironments } = useSelector((state) => state.globalEnvironments);
|
||||||
|
const { sidebarCollapsed, isCreatingCollection } = useSelector((state) => state.app);
|
||||||
|
|
||||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
|
||||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||||
const [importData, setImportData] = useState(null);
|
const [importData, setImportData] = useState(null);
|
||||||
@@ -29,6 +29,10 @@ const WorkspaceOverview = ({ workspace }) => {
|
|||||||
const workspaceEnvironmentsCount = globalEnvironments?.length || 0;
|
const workspaceEnvironmentsCount = globalEnvironments?.length || 0;
|
||||||
|
|
||||||
const handleCreateCollection = async () => {
|
const handleCreateCollection = async () => {
|
||||||
|
if (isCreatingCollection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!workspace?.pathname) {
|
if (!workspace?.pathname) {
|
||||||
toast.error('Workspace path not found');
|
toast.error('Workspace path not found');
|
||||||
return;
|
return;
|
||||||
@@ -37,7 +41,10 @@ const WorkspaceOverview = ({ workspace }) => {
|
|||||||
try {
|
try {
|
||||||
const { ipcRenderer } = window;
|
const { ipcRenderer } = window;
|
||||||
await ipcRenderer.invoke('renderer:ensure-collections-folder', workspace.pathname);
|
await ipcRenderer.invoke('renderer:ensure-collections-folder', workspace.pathname);
|
||||||
setCreateCollectionModalOpen(true);
|
if (sidebarCollapsed) {
|
||||||
|
dispatch(toggleSidebarCollapse());
|
||||||
|
}
|
||||||
|
dispatch(setIsCreatingCollection(true));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error ensuring collections folder exists:', error);
|
console.error('Error ensuring collections folder exists:', error);
|
||||||
toast.error('Error preparing workspace for collection creation');
|
toast.error('Error preparing workspace for collection creation');
|
||||||
@@ -87,10 +94,6 @@ const WorkspaceOverview = ({ workspace }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
{createCollectionModalOpen && (
|
|
||||||
<CreateCollection onClose={() => setCreateCollectionModalOpen(false)} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{importCollectionModalOpen && (
|
{importCollectionModalOpen && (
|
||||||
<ImportCollection
|
<ImportCollection
|
||||||
onClose={() => setImportCollectionModalOpen(false)}
|
onClose={() => setImportCollectionModalOpen(false)}
|
||||||
@@ -142,6 +145,7 @@ const WorkspaceOverview = ({ workspace }) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
icon={<IconPlus size={14} strokeWidth={1.5} />}
|
icon={<IconPlus size={14} strokeWidth={1.5} />}
|
||||||
onClick={handleCreateCollection}
|
onClick={handleCreateCollection}
|
||||||
|
disabled={isCreatingCollection}
|
||||||
>
|
>
|
||||||
Create Collection
|
Create Collection
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ const initialState = {
|
|||||||
envVarSearch: {
|
envVarSearch: {
|
||||||
collection: { query: '', expanded: false },
|
collection: { query: '', expanded: false },
|
||||||
global: { query: '', expanded: false }
|
global: { query: '', expanded: false }
|
||||||
}
|
},
|
||||||
|
isCreatingCollection: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export const appSlice = createSlice({
|
export const appSlice = createSlice({
|
||||||
@@ -157,6 +158,9 @@ export const appSlice = createSlice({
|
|||||||
setEnvVarSearchExpanded: (state, { payload: { context, expanded } }) => {
|
setEnvVarSearchExpanded: (state, { payload: { context, expanded } }) => {
|
||||||
if (!state.envVarSearch[context]) return;
|
if (!state.envVarSearch[context]) return;
|
||||||
state.envVarSearch[context].expanded = expanded;
|
state.envVarSearch[context].expanded = expanded;
|
||||||
|
},
|
||||||
|
setIsCreatingCollection: (state, action) => {
|
||||||
|
state.isCreatingCollection = action.payload;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
@@ -200,7 +204,8 @@ export const {
|
|||||||
setGitVersion,
|
setGitVersion,
|
||||||
setClipboard,
|
setClipboard,
|
||||||
setEnvVarSearchQuery,
|
setEnvVarSearchQuery,
|
||||||
setEnvVarSearchExpanded
|
setEnvVarSearchExpanded,
|
||||||
|
setIsCreatingCollection
|
||||||
} = appSlice.actions;
|
} = appSlice.actions;
|
||||||
|
|
||||||
export const savePreferences = (preferences) => (dispatch, getState) => {
|
export const savePreferences = (preferences) => (dispatch, getState) => {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { removeCollection, addTransientDirectory, updateCollectionMountStatus }
|
|||||||
import { updateGlobalEnvironments } from '../global-environments';
|
import { updateGlobalEnvironments } from '../global-environments';
|
||||||
import { addTab, focusTab } from '../tabs';
|
import { addTab, focusTab } from '../tabs';
|
||||||
import { normalizePath } from 'utils/common/path';
|
import { normalizePath } from 'utils/common/path';
|
||||||
|
import { sanitizeName } from 'utils/common/regex';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
const { ipcRenderer } = window;
|
const { ipcRenderer } = window;
|
||||||
@@ -50,6 +51,21 @@ const transformCollection = async (collection, type) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a workspace with a unique name under the given location
|
||||||
|
*/
|
||||||
|
export const createWorkspaceWithUniqueName = (location) => {
|
||||||
|
return async (dispatch) => {
|
||||||
|
const name = await ipcRenderer?.invoke('renderer:find-unique-folder-name', 'untitled workspace', location) || 'untitled workspace';
|
||||||
|
const folderName = sanitizeName(name);
|
||||||
|
const result = await dispatch(createWorkspaceAction(name, folderName, location));
|
||||||
|
if (result?.workspaceUid) {
|
||||||
|
dispatch(updateWorkspace({ uid: result.workspaceUid, isNewlyCreated: true }));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const createWorkspaceAction = (workspaceName, workspaceFolderName, workspaceLocation) => {
|
export const createWorkspaceAction = (workspaceName, workspaceFolderName, workspaceLocation) => {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const {
|
|||||||
isFile,
|
isFile,
|
||||||
isDirectory
|
isDirectory
|
||||||
} = require('../utils/filesystem');
|
} = require('../utils/filesystem');
|
||||||
|
const { findUniqueFolderName } = require('../utils/collection-import');
|
||||||
|
|
||||||
const registerFilesystemIpc = (mainWindow) => {
|
const registerFilesystemIpc = (mainWindow) => {
|
||||||
ipcMain.handle('renderer:browse-directory', async (event, pathname, request) => {
|
ipcMain.handle('renderer:browse-directory', async (event, pathname, request) => {
|
||||||
@@ -47,6 +48,14 @@ const registerFilesystemIpc = (mainWindow) => {
|
|||||||
ipcMain.handle('renderer:is-directory', async (_, pathname) => {
|
ipcMain.handle('renderer:is-directory', async (_, pathname) => {
|
||||||
return isDirectory(pathname);
|
return isDirectory(pathname);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('renderer:find-unique-folder-name', async (_, baseName, location) => {
|
||||||
|
try {
|
||||||
|
return await findUniqueFolderName(baseName, location);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = registerFilesystemIpc;
|
module.exports = registerFilesystemIpc;
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ test.describe('Default Location Feature', () => {
|
|||||||
await page.getByTestId('collections-header-add-menu').click();
|
await page.getByTestId('collections-header-add-menu').click();
|
||||||
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
|
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
|
||||||
|
|
||||||
|
// Wait for inline creator to appear, then click the cog button to open advanced modal
|
||||||
|
const inlineCreator = page.locator('.inline-collection-creator');
|
||||||
|
await inlineCreator.waitFor({ state: 'visible', timeout: 5000 });
|
||||||
|
await inlineCreator.locator('.cog-btn').click();
|
||||||
|
|
||||||
// Wait for modal to be visible
|
// Wait for modal to be visible
|
||||||
await page.locator('.bruno-modal').waitFor({ state: 'visible' });
|
await page.locator('.bruno-modal').waitFor({ state: 'visible' });
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,13 @@ const createCollection = async (page, collectionName: string, collectionLocation
|
|||||||
await page.getByTestId('collections-header-add-menu').click();
|
await page.getByTestId('collections-header-add-menu').click();
|
||||||
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
|
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
|
||||||
|
|
||||||
|
// Wait for inline creator to appear, then click the cog button to open advanced modal
|
||||||
|
const inlineCreator = page.locator('.inline-collection-creator');
|
||||||
|
await inlineCreator.waitFor({ state: 'visible', timeout: 5000 });
|
||||||
|
await inlineCreator.locator('.cog-btn').click();
|
||||||
|
|
||||||
const createCollectionModal = page.locator('.bruno-modal-card').filter({ hasText: 'Create Collection' });
|
const createCollectionModal = page.locator('.bruno-modal-card').filter({ hasText: 'Create Collection' });
|
||||||
|
await createCollectionModal.waitFor({ state: 'visible', timeout: 5000 });
|
||||||
|
|
||||||
await createCollectionModal.getByLabel('Name').fill(collectionName);
|
await createCollectionModal.getByLabel('Name').fill(collectionName);
|
||||||
const locationInput = createCollectionModal.getByLabel('Location');
|
const locationInput = createCollectionModal.getByLabel('Location');
|
||||||
|
|||||||
Reference in New Issue
Block a user