Optimize sidebar collapsing

This commit is contained in:
Gregory Schier
2024-12-23 05:05:04 -08:00
parent 61d094d9fd
commit 31f2bff0f6
35 changed files with 402 additions and 238 deletions

View File

@@ -64,7 +64,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
const recentWorkspaces = useRecentWorkspaces();
const requests = useRequests();
const activeRequest = useActiveRequest();
const recentRequests = useRecentRequests();
const [recentRequests] = useRecentRequests();
const openWorkspace = useOpenWorkspace();
const createWorkspace = useCreateWorkspace();
const createHttpRequest = useCreateHttpRequest();
@@ -78,6 +78,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
const [, setSidebarHidden] = useSidebarHidden();
const openSettings = useOpenSettings();
const navigate = useNavigate();
const { baseEnvironment } = useEnvironments();
const workspaceCommands = useMemo<CommandPaletteItem[]>(() => {
const commands: CommandPaletteItem[] = [
@@ -131,7 +132,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
{
key: 'environment.create',
label: 'Create Environment',
onSelect: createEnvironment.mutate,
onSelect: () => createEnvironment.mutate(baseEnvironment),
},
{
key: 'sidebar.toggle',
@@ -180,7 +181,8 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
activeCookieJar?.id,
activeEnvironment,
activeRequest,
createEnvironment.mutate,
baseEnvironment,
createEnvironment,
createGrpcRequest,
createHttpRequest,
createWorkspace.mutate,
@@ -375,6 +377,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
const index = filteredAllItems.findIndex((v) => v.key === selectedItem?.key);
console.log("ENDER", e.key);
if (e.key === 'ArrowDown' || (e.ctrlKey && e.key === 'n')) {
const next = filteredAllItems[index + 1] ?? filteredAllItems[0];

View File

@@ -1,9 +1,10 @@
import { emit } from '@tauri-apps/api/event';
import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugin';
import { useEnsureActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useEnsureActiveCookieJar, useSubscribeActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useSubscribeActiveEnvironmentId } from '../hooks/useActiveEnvironment';
import { useActiveRequest } from '../hooks/useActiveRequest';
import {useSubscribeActiveRequestId} from "../hooks/useActiveRequestId";
import {useSubscribeActiveWorkspaceId} from "../hooks/useActiveWorkspace";
import { useSubscribeActiveRequestId } from '../hooks/useActiveRequestId';
import { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace';
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest';
import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest';
@@ -14,13 +15,14 @@ import { useNotificationToast } from '../hooks/useNotificationToast';
import { usePrompt } from '../hooks/usePrompt';
import { useRecentCookieJars } from '../hooks/useRecentCookieJars';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { useSubscribeRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting';
import { useSyncModelStores } from '../hooks/useSyncModelStores';
import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels';
import {useSyncWorkspaceRequestTitle} from "../hooks/useSyncWorkspaceRequestTitle";
import { useSyncWorkspaceRequestTitle } from '../hooks/useSyncWorkspaceRequestTitle';
import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting';
import { useSubscribeTemplateFunctions } from '../hooks/useTemplateFunctions';
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';
export function GlobalHooks() {
@@ -36,8 +38,11 @@ export function GlobalHooks() {
useRecentWorkspaces();
useRecentEnvironments();
useRecentCookieJars();
useRecentRequests();
useSubscribeRecentRequests();
useSyncWorkspaceChildModels();
useSubscribeTemplateFunctions();
useSubscribeActiveEnvironmentId();
useSubscribeActiveCookieJar();
// Other useful things
useNotificationToast();

View File

@@ -18,7 +18,7 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
const dropdownRef = useRef<DropdownRef>(null);
const activeRequest = useActiveRequest();
const activeWorkspace = useActiveWorkspace();
const allRecentRequestIds = useRecentRequests();
const [allRecentRequestIds] = useRecentRequests();
const recentRequestIds = useMemo(() => allRecentRequestIds.slice(1), [allRecentRequestIds]);
const requests = useRequests();
const navigate = useNavigate();

View File

@@ -22,18 +22,19 @@ export function RedirectToLatestWorkspace() {
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? null;
const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? null;
const requestId = (await getRecentRequests(workspaceId))[0] ?? null;
const search = { cookie_jar_id: cookieJarId, environment_id: environmentId };
if (workspaceId != null && requestId != null) {
await navigate({
to: '/workspaces/$workspaceId/requests/$requestId',
params: { workspaceId, requestId },
search: { cookieJarId, environmentId },
search,
});
} else {
await navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId },
search: { cookieJarId, environmentId },
search,
});
}
})();

View File

@@ -140,8 +140,28 @@ export const RequestPane = memo(function RequestPane({
value: activeRequest.bodyType,
items: [
{ type: 'separator', label: 'Form Data' },
{ label: 'Url Encoded', value: BODY_TYPE_FORM_URLENCODED },
{ label: 'Multi-Part', value: BODY_TYPE_FORM_MULTIPART },
{
label: (
<>
Url Encoded
<CountBadge
count={'form' in activeRequest.body && activeRequest.body.form.length}
/>
</>
),
value: BODY_TYPE_FORM_URLENCODED,
},
{
label: (
<>
Url Encoded
<CountBadge
count={'form' in activeRequest.body && activeRequest.body.form.length}
/>
</>
),
value: BODY_TYPE_FORM_MULTIPART,
},
{ type: 'separator', label: 'Text Content' },
{ label: 'GraphQL', value: BODY_TYPE_GRAPHQL },
{ label: 'JSON', value: BODY_TYPE_JSON },
@@ -252,6 +272,7 @@ export const RequestPane = memo(function RequestPane({
[
activeRequest.authentication,
activeRequest.authenticationType,
activeRequest.body,
activeRequest.bodyType,
activeRequest.description,
activeRequest.headers,

View File

@@ -1,27 +1,33 @@
import { useNavigate } from '@tanstack/react-router';
import { useRouteError } from 'react-router-dom';
import { Button } from './core/Button';
import { FormattedError } from './core/FormattedError';
import { Heading } from './core/Heading';
import { VStack } from './core/Stacks';
export default function RouteError() {
const navigate = useNavigate();
const error = useRouteError();
export default function RouteError({ error }: { error: unknown; reset: () => void }) {
console.log('Error', error);
const stringified = JSON.stringify(error);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const message = (error as any).message ?? stringified;
const stack =
typeof error === 'object' && error != null && 'stack' in error ? String(error.stack) : null;
return (
<div className="flex items-center justify-center h-full">
<VStack space={5} className="max-w-[50rem] !h-auto">
<VStack space={5} className="w-[50rem] !h-auto">
<Heading>Route Error 🔥</Heading>
<FormattedError>{message}</FormattedError>
<FormattedError>
{message}
{stack && (
<details className="mt-3 select-autotext-xs">
<summary className="!cursor-default !select-none">Stack Trace</summary>
<div className="mt-2 text-xs">{stack}</div>
</details>
)}
</FormattedError>
<VStack space={2}>
<Button
color="primary"
onClick={async () => {
await navigate({ to: '/workspaces' });
window.location.assign('/');
}}
>
Go Home

View File

@@ -11,9 +11,9 @@ import { useFolders } from '../hooks/useFolders';
import { useGrpcConnections } from '../hooks/useGrpcConnections';
import { useHotKey } from '../hooks/useHotKey';
import { useHttpResponses } from '../hooks/useHttpResponses';
import { useKeyValue } from '../hooks/useKeyValue';
import { useRequests } from '../hooks/useRequests';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { getSidebarCollapsedMap } from '../hooks/useSidebarItemCollapsed';
import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
@@ -50,13 +50,6 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
const [hoveredTree, setHoveredTree] = useState<SidebarTreeNode | null>(null);
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const navigate = useNavigate();
const { value: collapsed, set: setCollapsed } = useKeyValue<Record<string, boolean>>({
key: ['sidebar_collapsed', activeWorkspace?.id ?? 'n/a'],
fallback: {},
namespace: 'no_sync',
});
const isCollapsed = useCallback((id: string) => collapsed?.[id] ?? false, [collapsed]);
const { tree, treeParentMap, selectableRequests } = useMemo<{
tree: SidebarTreeNode | null;
@@ -97,7 +90,6 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
const childItems = childrenMap[node.item.id] ?? [];
// Recurse to children
const isCollapsed = collapsed?.[node.item.id];
const depth = node.depth + 1;
childItems.sort((a, b) => a.sortPriority - b.sortPriority);
for (const item of childItems) {
@@ -105,7 +97,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
// Add to children
node.children.push(next({ item, children: [], depth }));
// Add to selectable requests
if (item.model !== 'folder' && !isCollapsed) {
if (item.model !== 'folder') {
selectableRequests.push({ id: item.id, index: selectableRequestIndex++, tree: node });
}
}
@@ -116,7 +108,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
const tree = next({ item: activeWorkspace, children: [], depth: 0 });
return { tree, treeParentMap, selectableRequests, selectedRequest };
}, [activeWorkspace, requests, folders, collapsed]);
}, [activeWorkspace, requests, folders]);
const focusActiveRequest = useCallback(
(
@@ -160,9 +152,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
const { item } = node;
if (item.model === 'folder') {
await setCollapsed((c) => ({ ...c, [item.id]: !c[item.id] }));
} else {
if (item.model === 'http_request' || item.model === 'grpc_request') {
await navigate({
to: '/workspaces/$workspaceId/requests/$requestId',
params: {
@@ -177,7 +167,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
setSelectedTree(tree);
}
},
[treeParentMap, setCollapsed, navigate, setSelectedId],
[treeParentMap, navigate, setSelectedId],
);
const handleClearSelected = useCallback(() => {
@@ -267,13 +257,14 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
);
const handleMove = useCallback<SidebarItemProps['onMove']>(
(id, side) => {
async (id, side) => {
let hoveredTree = treeParentMap[id] ?? null;
const dragIndex = hoveredTree?.children.findIndex((n) => n.item.id === id) ?? -99;
const hoveredItem = hoveredTree?.children[dragIndex]?.item ?? null;
let hoveredIndex = dragIndex + (side === 'above' ? 0 : 1);
if (hoveredItem?.model === 'folder' && side === 'below' && !isCollapsed(hoveredItem.id)) {
const isHoveredItemCollapsed = hoveredItem != null ? getSidebarCollapsedMap()[hoveredItem.id] : false;
if (hoveredItem?.model === 'folder' && side === 'below' && !isHoveredItemCollapsed) {
// Move into the folder if it's open and we're moving below it
hoveredTree = hoveredTree?.children.find((n) => n.item.id === id) ?? null;
hoveredIndex = 0;
@@ -282,7 +273,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
setHoveredTree(hoveredTree);
setHoveredIndex(hoveredIndex);
},
[isCollapsed, treeParentMap],
[treeParentMap],
);
const handleDragStart = useCallback<SidebarItemProps['onDragStart']>((id: string) => {
@@ -385,7 +376,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
const mainContextMenuItems = useCreateDropdownItems();
// Not ready to render yet
if (tree == null || collapsed == null) {
if (tree == null) {
return null;
}
@@ -415,7 +406,6 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
<SidebarItems
treeParentMap={treeParentMap}
selectedTree={selectedTree}
isCollapsed={isCollapsed}
httpResponses={httpResponses}
grpcConnections={grpcConnections}
tree={tree}

View File

@@ -6,6 +6,7 @@ import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd';
import { activeRequestAtom } from '../hooks/useActiveRequest';
import { useScrollIntoView } from '../hooks/useScrollIntoView';
import { useSidebarItemCollapsed } from '../hooks/useSidebarItemCollapsed';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { jotaiStore } from '../lib/jotai';
@@ -15,7 +16,7 @@ import { Icon } from './core/Icon';
import { StatusTag } from './core/StatusTag';
import { RequestContextMenu } from './RequestContextMenu';
import type { SidebarTreeNode } from './Sidebar';
import {sidebarSelectedIdAtom} from "./SidebarAtoms";
import { sidebarSelectedIdAtom } from './SidebarAtoms';
import type { SidebarItemsProps } from './SidebarItems';
enum ItemTypes {
@@ -35,7 +36,7 @@ export type SidebarItemProps = {
child: SidebarTreeNode;
latestHttpResponse: HttpResponse | null;
latestGrpcConnection: GrpcConnection | null;
} & Pick<SidebarItemsProps, 'isCollapsed' | 'onSelect'>;
} & Pick<SidebarItemsProps, 'onSelect'>;
type DragItem = {
id: string;
@@ -51,7 +52,6 @@ export const SidebarItem = memo(function SidebarItem({
onEnd,
onDragStart,
onSelect,
isCollapsed,
className,
itemFallbackName,
latestHttpResponse,
@@ -59,6 +59,7 @@ export const SidebarItem = memo(function SidebarItem({
children,
}: SidebarItemProps) {
const ref = useRef<HTMLLIElement>(null);
const [collapsed, toggleCollapsed] = useSidebarItemCollapsed(itemId);
const [, connectDrop] = useDrop<DragItem, void>(
{
@@ -106,20 +107,21 @@ export const SidebarItem = memo(function SidebarItem({
const [selected, setSelected] = useState<boolean>(
jotaiStore.get(sidebarSelectedIdAtom) == itemId,
);
useEffect(() => {
jotaiStore.sub(sidebarSelectedIdAtom, () => {
useEffect(() => {
return jotaiStore.sub(sidebarSelectedIdAtom, () => {
const value = jotaiStore.get(sidebarSelectedIdAtom);
setSelected(value === itemId);
});
}, [itemId]);
const [active, setActive] = useState<boolean>(jotaiStore.get(activeRequestAtom)?.id === itemId);
useEffect(() => {
jotaiStore.sub(activeRequestAtom, () => {
const value = jotaiStore.get(activeRequestAtom);
setActive(value?.id === itemId);
});
}, [itemId]);
useEffect(
() =>
jotaiStore.sub(activeRequestAtom, () =>
setActive(jotaiStore.get(activeRequestAtom)?.id === itemId),
),
[itemId],
);
useScrollIntoView(ref.current, active);
@@ -175,7 +177,10 @@ export const SidebarItem = memo(function SidebarItem({
[handleSubmitNameEdit],
);
const handleSelect = useCallback(() => onSelect(itemId), [onSelect, itemId]);
const handleSelect = useCallback(async () => {
if (itemModel === 'folder') toggleCollapsed();
else onSelect(itemId);
}, [itemModel, toggleCollapsed, onSelect, itemId]);
const [showContextMenu, setShowContextMenu] = useState<{
x: number;
y: number;
@@ -214,7 +219,7 @@ export const SidebarItem = memo(function SidebarItem({
editing && 'ring-1 focus-within:ring-focus',
active && 'bg-surface-highlight text-text',
!active && 'text-text-subtle group-hover/item:text-text',
showContextMenu && '!text-text', // Show as "active" when context menu is open
showContextMenu && '!text-text', // Show as "active" when the context menu is open
)}
>
{itemModel === 'folder' && (
@@ -224,7 +229,7 @@ export const SidebarItem = memo(function SidebarItem({
className={classNames(
'text-text-subtlest',
'transition-transform',
!isCollapsed(itemId) && 'transform rotate-90',
!collapsed && 'transform rotate-90',
)}
/>
)}
@@ -259,7 +264,7 @@ export const SidebarItem = memo(function SidebarItem({
) : null}
</button>
</div>
{children}
{collapsed ? null : children}
</li>
);
});

View File

@@ -18,7 +18,6 @@ export interface SidebarItemsProps {
handleEnd: (id: string) => void;
handleDragStart: (id: string) => void;
onSelect: (requestId: string) => void;
isCollapsed: (id: string) => boolean;
httpResponses: HttpResponse[];
grpcConnections: GrpcConnection[];
}
@@ -29,7 +28,6 @@ export const SidebarItems = memo(function SidebarItems({
draggingId,
onSelect,
treeParentMap,
isCollapsed,
hoveredTree,
hoveredIndex,
handleEnd,
@@ -71,11 +69,9 @@ export const SidebarItems = memo(function SidebarItems({
onEnd={handleEnd}
onSelect={onSelect}
onDragStart={handleDragStart}
isCollapsed={isCollapsed}
child={child}
>
{child.item.model === 'folder' &&
!isCollapsed(child.item.id) &&
draggingId !== child.item.id && (
<SidebarItems
draggingId={draggingId}
@@ -86,7 +82,6 @@ export const SidebarItems = memo(function SidebarItems({
hoveredTree={hoveredTree}
httpResponses={httpResponses}
grpcConnections={grpcConnections}
isCollapsed={isCollapsed}
onSelect={onSelect}
selectedTree={selectedTree}
tree={child}

View File

@@ -19,7 +19,7 @@ import {
useRef,
} from 'react';
import { useActiveEnvironmentVariables } from '../../../hooks/useActiveEnvironmentVariables';
import {useDialog} from "../../../hooks/useDialog";
import { useDialog } from '../../../hooks/useDialog';
import { parseTemplate } from '../../../hooks/useParseTemplate';
import { useRequestEditor } from '../../../hooks/useRequestEditor';
import { useSettings } from '../../../hooks/useSettings';

View File

@@ -9,7 +9,9 @@ export function FormattedError({ children }: Props) {
return (
<pre
className={classNames(
'font-mono text-sm w-full select-auto cursor-text bg-surface-highlight p-3 rounded',
'cursor-text select-auto',
'[&_*]:cursor-text [&_*]:select-auto',
'font-mono text-sm w-full bg-surface-highlight p-3 rounded',
'whitespace-pre-wrap border border-danger border-dashed overflow-x-auto',
)}
>

View File

@@ -27,16 +27,13 @@ export function PlainInput({
onChange,
onFocus,
onPaste,
placeholder,
require,
rightSlot,
size = 'md',
type = 'text',
validate,
autoSelect,
step,
autoFocus,
readOnly,
...props
}: PlainInputProps) {
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
@@ -130,7 +127,6 @@ export function PlainInput({
id={id}
type={type === 'password' && !obscured ? 'text' : type}
defaultValue={defaultValue}
placeholder={placeholder}
autoComplete="off"
autoCapitalize="off"
autoCorrect="off"
@@ -139,9 +135,7 @@ export function PlainInput({
className={classNames(commonClassName, 'h-auto')}
onFocus={handleFocus}
onBlur={handleBlur}
autoFocus={autoFocus}
step={step}
readOnly={readOnly}
{...props}
/>
</HStack>
{type === 'password' && (

View File

@@ -7,7 +7,7 @@ import { Icon } from './Icon';
export type RadioDropdownItem<T = string | null> =
| {
type?: 'default';
label: string;
label: ReactNode;
shortLabel?: string;
value: T;
rightSlot?: ReactNode;