Merge remote-tracking branch 'origin/main' into cli-command-architecture

This commit is contained in:
Gregory Schier
2026-02-16 09:37:20 -08:00
4 changed files with 59 additions and 26 deletions

View File

@@ -3,23 +3,30 @@ import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-intern
import { MoveToWorkspaceDialog } from '../components/MoveToWorkspaceDialog'; import { MoveToWorkspaceDialog } from '../components/MoveToWorkspaceDialog';
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace'; import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
import { createFastMutation } from '../hooks/useFastMutation'; import { createFastMutation } from '../hooks/useFastMutation';
import { pluralizeCount } from '../lib/pluralize';
import { showDialog } from '../lib/dialog'; import { showDialog } from '../lib/dialog';
import { jotaiStore } from '../lib/jotai'; import { jotaiStore } from '../lib/jotai';
export const moveToWorkspace = createFastMutation({ export const moveToWorkspace = createFastMutation({
mutationKey: ['move_workspace'], mutationKey: ['move_workspace'],
mutationFn: async (request: HttpRequest | GrpcRequest | WebsocketRequest) => { mutationFn: async (requests: (HttpRequest | GrpcRequest | WebsocketRequest)[]) => {
const activeWorkspaceId = jotaiStore.get(activeWorkspaceIdAtom); const activeWorkspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (activeWorkspaceId == null) return; if (activeWorkspaceId == null) return;
if (requests.length === 0) return;
const title =
requests.length === 1
? 'Move Request'
: `Move ${pluralizeCount('Request', requests.length)}`;
showDialog({ showDialog({
id: 'change-workspace', id: 'change-workspace',
title: 'Move Workspace', title,
size: 'sm', size: 'sm',
render: ({ hide }) => ( render: ({ hide }) => (
<MoveToWorkspaceDialog <MoveToWorkspaceDialog
onDone={hide} onDone={hide}
request={request} requests={requests}
activeWorkspaceId={activeWorkspaceId} activeWorkspaceId={activeWorkspaceId}
/> />
), ),

View File

@@ -2,6 +2,7 @@ import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-intern
import { patchModel, workspacesAtom } from '@yaakapp-internal/models'; import { patchModel, workspacesAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { useState } from 'react'; import { useState } from 'react';
import { pluralizeCount } from '../lib/pluralize';
import { resolvedModelName } from '../lib/resolvedModelName'; import { resolvedModelName } from '../lib/resolvedModelName';
import { router } from '../lib/router'; import { router } from '../lib/router';
import { showToast } from '../lib/toast'; import { showToast } from '../lib/toast';
@@ -12,18 +13,21 @@ import { VStack } from './core/Stacks';
interface Props { interface Props {
activeWorkspaceId: string; activeWorkspaceId: string;
request: HttpRequest | GrpcRequest | WebsocketRequest; requests: (HttpRequest | GrpcRequest | WebsocketRequest)[];
onDone: () => void; onDone: () => void;
} }
export function MoveToWorkspaceDialog({ onDone, request, activeWorkspaceId }: Props) { export function MoveToWorkspaceDialog({ onDone, requests, activeWorkspaceId }: Props) {
const workspaces = useAtomValue(workspacesAtom); const workspaces = useAtomValue(workspacesAtom);
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string>(activeWorkspaceId); const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string>(activeWorkspaceId);
const targetWorkspace = workspaces.find((w) => w.id === selectedWorkspaceId);
const isSameWorkspace = selectedWorkspaceId === activeWorkspaceId;
return ( return (
<VStack space={4} className="mb-4"> <VStack space={4} className="mb-4">
<Select <Select
label="New Workspace" label="Target Workspace"
name="workspace" name="workspace"
value={selectedWorkspaceId} value={selectedWorkspaceId}
onChange={setSelectedWorkspaceId} onChange={setSelectedWorkspaceId}
@@ -34,27 +38,31 @@ export function MoveToWorkspaceDialog({ onDone, request, activeWorkspaceId }: Pr
/> />
<Button <Button
color="primary" color="primary"
disabled={selectedWorkspaceId === activeWorkspaceId} disabled={isSameWorkspace}
onClick={async () => { onClick={async () => {
const patch = { const patch = {
workspaceId: selectedWorkspaceId, workspaceId: selectedWorkspaceId,
folderId: null, folderId: null,
}; };
await patchModel(request, patch); await Promise.all(requests.map((r) => patchModel(r, patch)));
// Hide after a moment, to give time for request to disappear // Hide after a moment, to give time for requests to disappear
setTimeout(onDone, 100); setTimeout(onDone, 100);
showToast({ showToast({
id: 'workspace-moved', id: 'workspace-moved',
message: ( message:
<> requests.length === 1 && requests[0] != null ? (
<InlineCode>{resolvedModelName(request)}</InlineCode> moved to{' '} <>
<InlineCode> <InlineCode>{resolvedModelName(requests[0])}</InlineCode> moved to{' '}
{workspaces.find((w) => w.id === selectedWorkspaceId)?.name ?? 'unknown'} <InlineCode>{targetWorkspace?.name ?? 'unknown'}</InlineCode>
</InlineCode> </>
</> ) : (
), <>
{pluralizeCount('request', requests.length)} moved to{' '}
<InlineCode>{targetWorkspace?.name ?? 'unknown'}</InlineCode>
</>
),
action: ({ hide }) => ( action: ({ hide }) => (
<Button <Button
size="xs" size="xs"
@@ -74,7 +82,7 @@ export function MoveToWorkspaceDialog({ onDone, request, activeWorkspaceId }: Pr
}); });
}} }}
> >
Move {requests.length === 1 ? 'Move' : `Move ${pluralizeCount('Request', requests.length)}`}
</Button> </Button>
</VStack> </VStack>
); );

View File

@@ -278,6 +278,7 @@ function Sidebar({ className }: { className?: string }) {
}, },
}, },
'sidebar.selected.duplicate': { 'sidebar.selected.duplicate': {
// Higher priority so this takes precedence over model.duplicate (same Meta+d binding)
priority: 10, priority: 10,
enable, enable,
cb: async (items: SidebarModel[]) => { cb: async (items: SidebarModel[]) => {
@@ -290,6 +291,18 @@ function Sidebar({ className }: { className?: string }) {
} }
}, },
}, },
'sidebar.selected.move': {
enable,
cb: async (items: SidebarModel[]) => {
const requests = items.filter(
(i): i is HttpRequest | GrpcRequest | WebsocketRequest =>
i.model === 'http_request' || i.model === 'grpc_request' || i.model === 'websocket_request'
);
if (requests.length > 0) {
moveToWorkspace.mutate(requests);
}
},
},
'request.send': { 'request.send': {
enable, enable,
cb: async (items: SidebarModel[]) => { cb: async (items: SidebarModel[]) => {
@@ -320,6 +333,10 @@ function Sidebar({ className }: { className?: string }) {
const workspaces = jotaiStore.get(workspacesAtom); const workspaces = jotaiStore.get(workspacesAtom);
const onlyHttpRequests = items.every((i) => i.model === 'http_request'); const onlyHttpRequests = items.every((i) => i.model === 'http_request');
const requestItems = items.filter(
(i) =>
i.model === 'http_request' || i.model === 'grpc_request' || i.model === 'websocket_request',
);
const initialItems: ContextMenuProps['items'] = [ const initialItems: ContextMenuProps['items'] = [
{ {
@@ -416,16 +433,13 @@ function Sidebar({ className }: { className?: string }) {
onSelect: () => actions['sidebar.selected.duplicate'].cb(items), onSelect: () => actions['sidebar.selected.duplicate'].cb(items),
}, },
{ {
label: 'Move', label: items.length <= 1 ? 'Move' : `Move ${requestItems.length} Requests`,
hotKeyAction: 'sidebar.selected.move',
hotKeyLabelOnly: true,
leftSlot: <Icon icon="arrow_right_circle" />, leftSlot: <Icon icon="arrow_right_circle" />,
hidden: hidden: workspaces.length <= 1 || requestItems.length === 0 || requestItems.length !== items.length,
workspaces.length <= 1 ||
items.length > 1 ||
child.model === 'folder' ||
child.model === 'workspace',
onSelect: () => { onSelect: () => {
if (child.model === 'folder' || child.model === 'workspace') return; actions['sidebar.selected.move'].cb(items);
moveToWorkspace.mutate(child);
}, },
}, },
{ {

View File

@@ -28,6 +28,7 @@ export type HotkeyAction =
| 'sidebar.filter' | 'sidebar.filter'
| 'sidebar.selected.delete' | 'sidebar.selected.delete'
| 'sidebar.selected.duplicate' | 'sidebar.selected.duplicate'
| 'sidebar.selected.move'
| 'sidebar.selected.rename' | 'sidebar.selected.rename'
| 'sidebar.expand_all' | 'sidebar.expand_all'
| 'sidebar.collapse_all' | 'sidebar.collapse_all'
@@ -58,6 +59,7 @@ const defaultHotkeysMac: Record<HotkeyAction, string[]> = {
'sidebar.collapse_all': ['Meta+Shift+Minus'], 'sidebar.collapse_all': ['Meta+Shift+Minus'],
'sidebar.selected.delete': ['Delete', 'Meta+Backspace'], 'sidebar.selected.delete': ['Delete', 'Meta+Backspace'],
'sidebar.selected.duplicate': ['Meta+d'], 'sidebar.selected.duplicate': ['Meta+d'],
'sidebar.selected.move': [],
'sidebar.selected.rename': ['Enter'], 'sidebar.selected.rename': ['Enter'],
'sidebar.focus': ['Meta+b'], 'sidebar.focus': ['Meta+b'],
'sidebar.context_menu': ['Control+Enter'], 'sidebar.context_menu': ['Control+Enter'],
@@ -87,6 +89,7 @@ const defaultHotkeysOther: Record<HotkeyAction, string[]> = {
'sidebar.collapse_all': ['Control+Shift+Minus'], 'sidebar.collapse_all': ['Control+Shift+Minus'],
'sidebar.selected.delete': ['Delete', 'Control+Backspace'], 'sidebar.selected.delete': ['Delete', 'Control+Backspace'],
'sidebar.selected.duplicate': ['Control+d'], 'sidebar.selected.duplicate': ['Control+d'],
'sidebar.selected.move': [],
'sidebar.selected.rename': ['Enter'], 'sidebar.selected.rename': ['Enter'],
'sidebar.focus': ['Control+b'], 'sidebar.focus': ['Control+b'],
'sidebar.context_menu': ['Alt+Insert'], 'sidebar.context_menu': ['Alt+Insert'],
@@ -141,6 +144,7 @@ const hotkeyLabels: Record<HotkeyAction, string> = {
'sidebar.collapse_all': 'Collapse All Folders', 'sidebar.collapse_all': 'Collapse All Folders',
'sidebar.selected.delete': 'Delete Selected Sidebar Item', 'sidebar.selected.delete': 'Delete Selected Sidebar Item',
'sidebar.selected.duplicate': 'Duplicate Selected Sidebar Item', 'sidebar.selected.duplicate': 'Duplicate Selected Sidebar Item',
'sidebar.selected.move': 'Move Selected to Workspace',
'sidebar.selected.rename': 'Rename Selected Sidebar Item', 'sidebar.selected.rename': 'Rename Selected Sidebar Item',
'sidebar.focus': 'Focus or Toggle Sidebar', 'sidebar.focus': 'Focus or Toggle Sidebar',
'sidebar.context_menu': 'Show Context Menu', 'sidebar.context_menu': 'Show Context Menu',