mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-05-05 23:43:45 +02:00
Move Tree component to @yaakapp-internal/ui package
Decouple Tree from client app's hotkey system by adding getSelectedItems() to TreeHandle and having callers register hotkeys externally. Extract shared action callbacks to eliminate duplication between hotkey and context menu handlers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
208
PLAN.md
208
PLAN.md
@@ -1,208 +0,0 @@
|
||||
# Unified Actions System (Proxy App)
|
||||
|
||||
## Context
|
||||
|
||||
The proxy app is greenfield — no existing hotkeys, context menus, or command palette to migrate. This is an opportunity to build the actions system right from the start, so every interactive feature is powered by a single shared registry.
|
||||
|
||||
## Goals
|
||||
|
||||
- One place to define every user-facing action (label, icon, default hotkey)
|
||||
- Actions can be triggered from hotkeys, context menus, command palette, native menus, or toolbar buttons
|
||||
- Rust-defined enums exported to TypeScript via ts-rs for type safety
|
||||
- Actions are either **Core** (handled in Rust) or **Frontend** (handled in TypeScript)
|
||||
- All dispatch goes through one Rust `execute_action()` function — callable as an RPC command (from frontend) or directly (from native menus / Rust code)
|
||||
|
||||
## Relationship to RPC
|
||||
|
||||
Actions sit **on top of** the RPC layer. RPC is the transport; actions are user intent.
|
||||
|
||||
- `execute_action` is an RPC command — the frontend calls it to trigger any action
|
||||
- Core action handlers live in Rust and contain the business logic directly
|
||||
- Frontend action handlers live in TypeScript — when Rust receives a frontend action, it emits a Tauri event and the frontend listener handles it
|
||||
- Actions carry no params — they use defaults or derive what they need from scope context (e.g., `http_exchange_id`). Standalone RPC commands like `proxy_start`/`proxy_stop` go away — `execute_action` is the only entry point.
|
||||
|
||||
```
|
||||
Native Tauri menu / Rust code
|
||||
→ execute_action(ActionInvocation)
|
||||
→ Core? → call handler directly
|
||||
→ Frontend? → emit event to frontend → frontend handles
|
||||
|
||||
Frontend hotkey / context menu / command palette
|
||||
→ rpc("execute_action", ActionInvocation)
|
||||
→ same execute_action() function
|
||||
→ Core? → call handler directly
|
||||
→ Frontend? → emit event back to frontend → frontend handles
|
||||
```
|
||||
|
||||
## Scopes
|
||||
|
||||
| Scope | Context | Example actions |
|
||||
|-------|---------|-----------------|
|
||||
| `Global` | (none) | start/stop proxy, clear history, zoom, toggle command palette |
|
||||
| `HttpExchange` | `http_exchange_id: String` | view details, copy URL, copy as cURL, delete, replay |
|
||||
|
||||
Start small — more scopes can be added later as the app grows.
|
||||
|
||||
## Rust Types
|
||||
|
||||
### Action enums (per scope, split Core / Frontend)
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "gen_actions.ts")]
|
||||
pub enum GlobalCoreAction {
|
||||
ProxyStart,
|
||||
ProxyStop,
|
||||
ClearHistory,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "gen_actions.ts")]
|
||||
pub enum GlobalFrontendAction {
|
||||
ToggleCommandPalette,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
ZoomReset,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "gen_actions.ts")]
|
||||
pub enum HttpExchangeCoreAction {
|
||||
Delete,
|
||||
Replay,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "gen_actions.ts")]
|
||||
pub enum HttpExchangeFrontendAction {
|
||||
ViewDetails,
|
||||
CopyUrl,
|
||||
CopyAsCurl,
|
||||
}
|
||||
```
|
||||
|
||||
### Invocation enum
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(tag = "scope", rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "gen_actions.ts")]
|
||||
pub enum ActionInvocation {
|
||||
Global { action: GlobalAction },
|
||||
HttpExchange { action: HttpExchangeAction, http_exchange_id: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(tag = "handler", content = "action", rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "gen_actions.ts")]
|
||||
pub enum GlobalAction {
|
||||
Core(GlobalCoreAction),
|
||||
Frontend(GlobalFrontendAction),
|
||||
}
|
||||
// same for HttpExchangeAction
|
||||
```
|
||||
|
||||
### Action metadata
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_actions.ts")]
|
||||
pub struct ActionMetadata {
|
||||
pub label: String,
|
||||
pub icon: Option<String>,
|
||||
pub default_hotkey_mac: Option<Vec<String>>,
|
||||
pub default_hotkey_other: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub fn action_metadata(action: &ActionInvocation) -> ActionMetadata { ... }
|
||||
```
|
||||
|
||||
### Dispatch function
|
||||
|
||||
```rust
|
||||
pub fn execute_action(ctx: &ProxyCtx, invocation: ActionInvocation) -> Result<(), RpcError> {
|
||||
match invocation {
|
||||
ActionInvocation::Global { action: GlobalAction::Core(a) } => match a {
|
||||
GlobalCoreAction::ProxyStart => {
|
||||
// Start proxy on default port (9090)
|
||||
// Business logic lives here, not in a separate RPC command
|
||||
let mut handle = ctx.handle.lock()...;
|
||||
let proxy_handle = yaak_proxy::start_proxy(9090)?;
|
||||
// ...
|
||||
Ok(())
|
||||
}
|
||||
GlobalCoreAction::ProxyStop => {
|
||||
let mut handle = ctx.handle.lock()...;
|
||||
handle.take(); // Drop stops the proxy
|
||||
Ok(())
|
||||
}
|
||||
GlobalCoreAction::ClearHistory => { /* ... */ Ok(()) }
|
||||
},
|
||||
ActionInvocation::Global { action: GlobalAction::Frontend(_) } => {
|
||||
// Emit event — frontend listener handles it
|
||||
ctx.events.emit("action_invoke", &invocation);
|
||||
Ok(())
|
||||
}
|
||||
ActionInvocation::HttpExchange { action, http_exchange_id } => {
|
||||
// similar pattern
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Side
|
||||
|
||||
```typescript
|
||||
// Dispatch any action — always goes through Rust
|
||||
async function dispatch(invocation: ActionInvocation) {
|
||||
await rpc("execute_action", invocation);
|
||||
}
|
||||
|
||||
// Listen for frontend actions emitted by Rust
|
||||
listen("action_invoke", (invocation: ActionInvocation) => {
|
||||
// Route to the right handler
|
||||
const handler = frontendHandlers[invocation.scope]?.[invocation.action.action];
|
||||
handler?.(invocation);
|
||||
});
|
||||
|
||||
// Type-safe exhaustive handlers
|
||||
type FrontendHandlers = {
|
||||
global: Record<GlobalFrontendAction, () => void>;
|
||||
http_exchange: Record<HttpExchangeFrontendAction, (ctx: { http_exchange_id: string }) => void>;
|
||||
};
|
||||
```
|
||||
|
||||
## Crate Location
|
||||
|
||||
`crates-proxy/yaak-proxy-actions/` — action enums, metadata, `execute_action()` function, ts-rs exports. Referenced by `yaak-proxy-lib` to register as an RPC command.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Rust action definitions + dispatch
|
||||
- Create `crates-proxy/yaak-proxy-actions/` with enums, `ActionInvocation`, metadata, `execute_action()`
|
||||
- ts-rs generates `bindings/gen_actions.ts`
|
||||
- Add `execute_action` to `define_rpc!` in `yaak-proxy-lib`
|
||||
|
||||
### Step 2: TypeScript dispatch + handlers
|
||||
- Create `apps/yaak-proxy/actions.ts`
|
||||
- Import generated types, define `FrontendHandlers`, wire `dispatch()`
|
||||
- Listen for `action_invoke` events (for Rust-initiated frontend actions)
|
||||
|
||||
### Step 3: Wire up UI
|
||||
- Toolbar buttons call `dispatch()` instead of inline `rpc()` calls
|
||||
- Add context menu on exchange table rows using action items
|
||||
- Build a basic command palette from the action registry
|
||||
|
||||
## Verification
|
||||
|
||||
- `cargo check -p yaak-proxy-actions`
|
||||
- `tsgo --noEmit` from repo root
|
||||
- Toolbar start/stop still works (now via actions)
|
||||
- Right-click exchange row shows context menu with correct labels
|
||||
- Command palette lists available actions
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { Environment, Workspace } from '@yaakapp-internal/models';
|
||||
import { duplicateModel, patchModel } from '@yaakapp-internal/models';
|
||||
import { atom, useAtomValue } from 'jotai';
|
||||
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { createSubEnvironmentAndActivate } from '../commands/createEnvironment';
|
||||
import { activeWorkspaceAtom, activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
|
||||
import {
|
||||
environmentsBreakdownAtom,
|
||||
useEnvironmentsBreakdown,
|
||||
} from '../hooks/useEnvironmentsBreakdown';
|
||||
import { useHotKey } from '../hooks/useHotKey';
|
||||
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
|
||||
import { jotaiStore } from '../lib/jotai';
|
||||
import { isBaseEnvironment, isSubEnvironment } from '../lib/model_util';
|
||||
@@ -16,7 +17,8 @@ import { showColorPicker } from '../lib/showColorPicker';
|
||||
import { Banner } from './core/Banner';
|
||||
import type { ContextMenuProps, DropdownItem } from './core/Dropdown';
|
||||
import { ContextMenu } from './core/Dropdown';
|
||||
import { Icon } from '@yaakapp-internal/ui';
|
||||
import { Icon, Tree } from '@yaakapp-internal/ui';
|
||||
import type { TreeNode, TreeHandle, TreeProps } from '@yaakapp-internal/ui';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { IconTooltip } from './core/IconTooltip';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
@@ -24,9 +26,6 @@ import type { PairEditorHandle } from './core/PairEditor';
|
||||
import { SplitLayout } from './core/SplitLayout';
|
||||
import { atomFamily } from 'jotai/utils';
|
||||
import { atomWithKVStorage } from '../lib/atoms/atomWithKVStorage';
|
||||
import type { TreeNode } from './core/tree/common';
|
||||
import type { TreeHandle, TreeProps } from './core/tree/Tree';
|
||||
import { Tree } from './core/tree/Tree';
|
||||
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
|
||||
import { EnvironmentEditor } from './EnvironmentEditor';
|
||||
import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip';
|
||||
@@ -137,44 +136,43 @@ function EnvironmentEditDialogSidebar({
|
||||
[baseEnvironment?.id, selectedEnvironmentId, setSelectedEnvironmentId],
|
||||
);
|
||||
|
||||
const actions = useMemo(() => {
|
||||
const enable = () => treeRef.current?.hasFocus() ?? false;
|
||||
const treeHasFocus = useCallback(() => treeRef.current?.hasFocus() ?? false, []);
|
||||
|
||||
const actions = {
|
||||
'sidebar.selected.rename': {
|
||||
enable,
|
||||
allowDefault: true,
|
||||
priority: 100,
|
||||
cb: async (items: TreeModel[]) => {
|
||||
const item = items[0];
|
||||
if (items.length === 1 && item != null) {
|
||||
treeRef.current?.renameItem(item.id);
|
||||
}
|
||||
},
|
||||
},
|
||||
'sidebar.selected.delete': {
|
||||
priority: 100,
|
||||
enable,
|
||||
cb: (items: TreeModel[]) => deleteModelWithConfirm(items),
|
||||
},
|
||||
'sidebar.selected.duplicate': {
|
||||
priority: 100,
|
||||
enable,
|
||||
cb: async (items: TreeModel[]) => {
|
||||
if (items.length === 1 && items[0]) {
|
||||
const item = items[0];
|
||||
const newId = await duplicateModel(item);
|
||||
setSelectedEnvironmentId(newId);
|
||||
} else {
|
||||
await Promise.all(items.map(duplicateModel));
|
||||
}
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
return actions;
|
||||
const getSelectedTreeModels = useCallback(
|
||||
() => treeRef.current?.getSelectedItems() as TreeModel[] | undefined,
|
||||
[],
|
||||
);
|
||||
|
||||
const handleRenameSelected = useCallback(() => {
|
||||
const items = getSelectedTreeModels();
|
||||
if (items?.length === 1 && items[0] != null) {
|
||||
treeRef.current?.renameItem(items[0].id);
|
||||
}
|
||||
}, [getSelectedTreeModels]);
|
||||
|
||||
const handleDeleteSelected = useCallback(
|
||||
(items: TreeModel[]) => deleteModelWithConfirm(items),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleDuplicateSelected = useCallback(async (items: TreeModel[]) => {
|
||||
if (items.length === 1 && items[0]) {
|
||||
const newId = await duplicateModel(items[0]);
|
||||
setSelectedEnvironmentId(newId);
|
||||
} else {
|
||||
await Promise.all(items.map(duplicateModel));
|
||||
}
|
||||
}, [setSelectedEnvironmentId]);
|
||||
|
||||
const hotkeys = useMemo<TreeProps<TreeModel>['hotkeys']>(() => ({ actions }), [actions]);
|
||||
useHotKey('sidebar.selected.rename', handleRenameSelected, { enable: treeHasFocus, allowDefault: true, priority: 100 });
|
||||
useHotKey('sidebar.selected.delete', useCallback(() => {
|
||||
const items = getSelectedTreeModels();
|
||||
if (items) handleDeleteSelected(items);
|
||||
}, [getSelectedTreeModels, handleDeleteSelected]), { enable: treeHasFocus, priority: 100 });
|
||||
useHotKey('sidebar.selected.duplicate', useCallback(async () => {
|
||||
const items = getSelectedTreeModels();
|
||||
if (items) await handleDuplicateSelected(items);
|
||||
}, [getSelectedTreeModels, handleDuplicateSelected]), { enable: treeHasFocus, priority: 100 });
|
||||
|
||||
const getContextMenu = useCallback(
|
||||
(items: TreeModel[]): ContextMenuProps['items'] => {
|
||||
@@ -203,12 +201,10 @@ function EnvironmentEditDialogSidebar({
|
||||
hidden: isBaseEnvironment(environment) || !singleEnvironment,
|
||||
hotKeyAction: 'sidebar.selected.rename',
|
||||
hotKeyLabelOnly: true,
|
||||
onSelect: async () => {
|
||||
onSelect: () => {
|
||||
// Not sure why this is needed, but without it the
|
||||
// edit input blurs immediately after opening.
|
||||
requestAnimationFrame(() => {
|
||||
actions['sidebar.selected.rename'].cb(items);
|
||||
});
|
||||
requestAnimationFrame(() => handleRenameSelected());
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -217,7 +213,7 @@ function EnvironmentEditDialogSidebar({
|
||||
hidden: isBaseEnvironment(environment),
|
||||
hotKeyAction: 'sidebar.selected.duplicate',
|
||||
hotKeyLabelOnly: true,
|
||||
onSelect: () => actions['sidebar.selected.duplicate'].cb(items),
|
||||
onSelect: () => handleDuplicateSelected(items),
|
||||
},
|
||||
{
|
||||
label: environment.color ? 'Change Color' : 'Assign Color',
|
||||
@@ -253,7 +249,7 @@ function EnvironmentEditDialogSidebar({
|
||||
|
||||
return menuItems;
|
||||
},
|
||||
[actions, baseEnvironments.length, handleDeleteEnvironment],
|
||||
[baseEnvironments.length, handleDeleteEnvironment, setSelectedEnvironmentId],
|
||||
);
|
||||
|
||||
const handleDragEnd = useCallback(async function handleDragEnd({
|
||||
@@ -317,7 +313,6 @@ function EnvironmentEditDialogSidebar({
|
||||
treeId={treeId}
|
||||
collapsedAtom={collapsedFamily(treeId)}
|
||||
className="px-2 pb-10"
|
||||
hotkeys={hotkeys}
|
||||
root={tree}
|
||||
getContextMenu={getContextMenu}
|
||||
renderContextMenu={renderContextMenuFn}
|
||||
|
||||
@@ -54,16 +54,18 @@ import { filter } from './core/Editor/filter/extension';
|
||||
import { evaluate, parseQuery } from './core/Editor/filter/query';
|
||||
import { HttpMethodTag } from './core/HttpMethodTag';
|
||||
import { HttpStatusTag } from './core/HttpStatusTag';
|
||||
import { Icon, LoadingIcon } from '@yaakapp-internal/ui';
|
||||
import {
|
||||
Icon,
|
||||
LoadingIcon,
|
||||
Tree,
|
||||
isSelectedFamily,
|
||||
selectedIdsFamily,
|
||||
} from '@yaakapp-internal/ui';
|
||||
import type { TreeNode, TreeHandle, TreeProps, TreeItemProps } from '@yaakapp-internal/ui';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import type { InputHandle } from './core/Input';
|
||||
import { Input } from './core/Input';
|
||||
import { isSelectedFamily, selectedIdsFamily } from './core/tree/atoms';
|
||||
import type { TreeNode } from './core/tree/common';
|
||||
import type { TreeHandle, TreeProps } from './core/tree/Tree';
|
||||
import { Tree } from './core/tree/Tree';
|
||||
import type { TreeItemProps } from './core/tree/TreeItem';
|
||||
import { atomWithKVStorage } from '../lib/atoms/atomWithKVStorage';
|
||||
import { GitDropdown } from './git/GitDropdown';
|
||||
|
||||
@@ -234,93 +236,98 @@ function Sidebar({ className }: { className?: string }) {
|
||||
[],
|
||||
);
|
||||
|
||||
const actions = useMemo(() => {
|
||||
const enable = () => treeRef.current?.hasFocus() ?? false;
|
||||
const treeHasFocus = useCallback(() => treeRef.current?.hasFocus() ?? false, []);
|
||||
|
||||
const actions = {
|
||||
'sidebar.context_menu': {
|
||||
enable,
|
||||
cb: () => treeRef.current?.showContextMenu(),
|
||||
},
|
||||
'sidebar.expand_all': {
|
||||
enable: isSidebarFocused,
|
||||
cb: () => {
|
||||
jotaiStore.set(collapsedFamily(treeId), {});
|
||||
},
|
||||
},
|
||||
'sidebar.collapse_all': {
|
||||
enable: isSidebarFocused,
|
||||
cb: () => {
|
||||
if (tree == null) return;
|
||||
const getSelectedTreeModels = useCallback(
|
||||
() => treeRef.current?.getSelectedItems() as SidebarModel[] | undefined,
|
||||
[],
|
||||
);
|
||||
|
||||
const next = (node: TreeNode<SidebarModel>, collapsed: Record<string, boolean>) => {
|
||||
let newCollapsed = { ...collapsed };
|
||||
for (const n of node.children ?? []) {
|
||||
if (n.item.model !== 'folder') continue;
|
||||
newCollapsed[n.item.id] = true;
|
||||
newCollapsed = next(n, newCollapsed);
|
||||
}
|
||||
return newCollapsed;
|
||||
};
|
||||
const collapsed = next(tree, {});
|
||||
jotaiStore.set(collapsedFamily(treeId), collapsed);
|
||||
},
|
||||
},
|
||||
'sidebar.selected.delete': {
|
||||
enable,
|
||||
cb: async (items: SidebarModel[]) => {
|
||||
await deleteModelWithConfirm(items);
|
||||
},
|
||||
},
|
||||
'sidebar.selected.rename': {
|
||||
enable,
|
||||
allowDefault: true,
|
||||
cb: async (items: SidebarModel[]) => {
|
||||
const item = items[0];
|
||||
if (items.length === 1 && item != null) {
|
||||
treeRef.current?.renameItem(item.id);
|
||||
}
|
||||
},
|
||||
},
|
||||
'sidebar.selected.duplicate': {
|
||||
// Higher priority so this takes precedence over model.duplicate (same Meta+d binding)
|
||||
priority: 10,
|
||||
enable,
|
||||
cb: async (items: SidebarModel[]) => {
|
||||
if (items.length === 1 && items[0]) {
|
||||
const item = items[0];
|
||||
const newId = await duplicateModel(item);
|
||||
navigateToRequestOrFolderOrWorkspace(newId, item.model);
|
||||
} else {
|
||||
await Promise.all(items.map(duplicateModel));
|
||||
}
|
||||
},
|
||||
},
|
||||
'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': {
|
||||
enable,
|
||||
cb: async (items: SidebarModel[]) => {
|
||||
await Promise.all(
|
||||
items
|
||||
.filter((i) => i.model === 'http_request')
|
||||
.map((i) => sendAnyHttpRequest.mutate(i.id)),
|
||||
);
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
return actions;
|
||||
}, [tree, treeId]);
|
||||
const handleRenameSelected = useCallback((items: SidebarModel[]) => {
|
||||
if (items.length === 1 && items[0] != null) {
|
||||
treeRef.current?.renameItem(items[0].id);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDeleteSelected = useCallback(
|
||||
async (items: SidebarModel[]) => { await deleteModelWithConfirm(items); },
|
||||
[],
|
||||
);
|
||||
|
||||
const handleDuplicateSelected = useCallback(async (items: SidebarModel[]) => {
|
||||
if (items.length === 1 && items[0]) {
|
||||
const newId = await duplicateModel(items[0]);
|
||||
navigateToRequestOrFolderOrWorkspace(newId, items[0].model);
|
||||
} else {
|
||||
await Promise.all(items.map(duplicateModel));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleMoveSelected = useCallback((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);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSendSelected = useCallback(async (items: SidebarModel[]) => {
|
||||
await Promise.all(
|
||||
items
|
||||
.filter((i) => i.model === 'http_request')
|
||||
.map((i) => sendAnyHttpRequest.mutate(i.id)),
|
||||
);
|
||||
}, []);
|
||||
|
||||
useHotKey('sidebar.context_menu', useCallback(() => {
|
||||
treeRef.current?.showContextMenu();
|
||||
}, []), { enable: treeHasFocus });
|
||||
|
||||
useHotKey('sidebar.expand_all', useCallback(() => {
|
||||
jotaiStore.set(collapsedFamily(treeId), {});
|
||||
}, [treeId]), { enable: isSidebarFocused });
|
||||
|
||||
useHotKey('sidebar.collapse_all', useCallback(() => {
|
||||
if (tree == null) return;
|
||||
const next = (node: TreeNode<SidebarModel>, collapsed: Record<string, boolean>) => {
|
||||
let newCollapsed = { ...collapsed };
|
||||
for (const n of node.children ?? []) {
|
||||
if (n.item.model !== 'folder') continue;
|
||||
newCollapsed[n.item.id] = true;
|
||||
newCollapsed = next(n, newCollapsed);
|
||||
}
|
||||
return newCollapsed;
|
||||
};
|
||||
const collapsed = next(tree, {});
|
||||
jotaiStore.set(collapsedFamily(treeId), collapsed);
|
||||
}, [tree, treeId]), { enable: isSidebarFocused });
|
||||
|
||||
useHotKey('sidebar.selected.delete', useCallback(() => {
|
||||
const items = getSelectedTreeModels();
|
||||
if (items) handleDeleteSelected(items);
|
||||
}, [getSelectedTreeModels, handleDeleteSelected]), { enable: treeHasFocus });
|
||||
|
||||
useHotKey('sidebar.selected.rename', useCallback(() => {
|
||||
const items = getSelectedTreeModels();
|
||||
if (items) handleRenameSelected(items);
|
||||
}, [getSelectedTreeModels, handleRenameSelected]), { enable: treeHasFocus, allowDefault: true });
|
||||
|
||||
useHotKey('sidebar.selected.duplicate', useCallback(async () => {
|
||||
const items = getSelectedTreeModels();
|
||||
if (items) await handleDuplicateSelected(items);
|
||||
}, [getSelectedTreeModels, handleDuplicateSelected]), { priority: 10, enable: treeHasFocus });
|
||||
|
||||
useHotKey('sidebar.selected.move', useCallback(() => {
|
||||
const items = getSelectedTreeModels();
|
||||
if (items) handleMoveSelected(items);
|
||||
}, [getSelectedTreeModels, handleMoveSelected]), { enable: treeHasFocus });
|
||||
|
||||
useHotKey('request.send', useCallback(async () => {
|
||||
const items = getSelectedTreeModels();
|
||||
if (items) await handleSendSelected(items);
|
||||
}, [getSelectedTreeModels, handleSendSelected]), { enable: treeHasFocus });
|
||||
|
||||
const getContextMenu = useCallback<(items: SidebarModel[]) => Promise<DropdownItem[]>>(
|
||||
async (items) => {
|
||||
@@ -356,7 +363,7 @@ function Sidebar({ className }: { className?: string }) {
|
||||
hotKeyLabelOnly: true,
|
||||
hidden: !onlyHttpRequests,
|
||||
leftSlot: <Icon icon="send_horizontal" />,
|
||||
onSelect: () => actions['request.send'].cb(items),
|
||||
onSelect: () => handleSendSelected(items),
|
||||
},
|
||||
...(items.length === 1 && child.model === 'http_request'
|
||||
? await getHttpRequestActions()
|
||||
@@ -426,16 +433,14 @@ function Sidebar({ className }: { className?: string }) {
|
||||
hidden: items.length > 1,
|
||||
hotKeyAction: 'sidebar.selected.rename',
|
||||
hotKeyLabelOnly: true,
|
||||
onSelect: () => {
|
||||
treeRef.current?.renameItem(child.id);
|
||||
},
|
||||
onSelect: () => handleRenameSelected(items),
|
||||
},
|
||||
{
|
||||
label: 'Duplicate',
|
||||
hotKeyAction: 'model.duplicate',
|
||||
hotKeyLabelOnly: true, // Would trigger for every request (bad)
|
||||
leftSlot: <Icon icon="copy" />,
|
||||
onSelect: () => actions['sidebar.selected.duplicate'].cb(items),
|
||||
onSelect: () => handleDuplicateSelected(items),
|
||||
},
|
||||
{
|
||||
label: items.length <= 1 ? 'Move' : `Move ${requestItems.length} Requests`,
|
||||
@@ -443,9 +448,7 @@ function Sidebar({ className }: { className?: string }) {
|
||||
hotKeyLabelOnly: true,
|
||||
leftSlot: <Icon icon="arrow_right_circle" />,
|
||||
hidden: workspaces.length <= 1 || requestItems.length === 0 || requestItems.length !== items.length,
|
||||
onSelect: () => {
|
||||
actions['sidebar.selected.move'].cb(items);
|
||||
},
|
||||
onSelect: () => handleMoveSelected(items),
|
||||
},
|
||||
{
|
||||
color: 'danger',
|
||||
@@ -453,13 +456,13 @@ function Sidebar({ className }: { className?: string }) {
|
||||
hotKeyAction: 'sidebar.selected.delete',
|
||||
hotKeyLabelOnly: true,
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
onSelect: () => actions['sidebar.selected.delete'].cb(items),
|
||||
onSelect: () => handleDeleteSelected(items),
|
||||
},
|
||||
...modelCreationItems,
|
||||
];
|
||||
return menuItems;
|
||||
},
|
||||
[actions],
|
||||
[],
|
||||
);
|
||||
|
||||
const renderContextMenuFn = useCallback<NonNullable<TreeProps<SidebarModel>['renderContextMenu']>>(
|
||||
@@ -469,8 +472,6 @@ function Sidebar({ className }: { className?: string }) {
|
||||
[],
|
||||
);
|
||||
|
||||
const hotkeys = useMemo<TreeProps<SidebarModel>['hotkeys']>(() => ({ actions }), [actions]);
|
||||
|
||||
// Use a language compartment for the filter so we can reconfigure it when the autocompletion changes
|
||||
const filterLanguageCompartmentRef = useRef(new Compartment());
|
||||
const filterCompartmentMountExtRef = useRef<Extension | null>(null);
|
||||
@@ -556,14 +557,26 @@ function Sidebar({ className }: { className?: string }) {
|
||||
{
|
||||
label: 'Expand All Folders',
|
||||
leftSlot: <Icon icon="chevrons_up_down" />,
|
||||
onSelect: actions['sidebar.expand_all'].cb,
|
||||
onSelect: () => jotaiStore.set(collapsedFamily(treeId), {}),
|
||||
hotKeyAction: 'sidebar.expand_all',
|
||||
hotKeyLabelOnly: true,
|
||||
},
|
||||
{
|
||||
label: 'Collapse All Folders',
|
||||
leftSlot: <Icon icon="chevrons_down_up" />,
|
||||
onSelect: actions['sidebar.collapse_all'].cb,
|
||||
onSelect: () => {
|
||||
if (tree == null) return;
|
||||
const next = (node: TreeNode<SidebarModel>, collapsed: Record<string, boolean>) => {
|
||||
let newCollapsed = { ...collapsed };
|
||||
for (const n of node.children ?? []) {
|
||||
if (n.item.model !== 'folder') continue;
|
||||
newCollapsed[n.item.id] = true;
|
||||
newCollapsed = next(n, newCollapsed);
|
||||
}
|
||||
return newCollapsed;
|
||||
};
|
||||
jotaiStore.set(collapsedFamily(treeId), next(tree, {}));
|
||||
},
|
||||
hotKeyAction: 'sidebar.collapse_all',
|
||||
hotKeyLabelOnly: true,
|
||||
},
|
||||
@@ -589,7 +602,6 @@ function Sidebar({ className }: { className?: string }) {
|
||||
root={tree}
|
||||
treeId={treeId}
|
||||
collapsedAtom={collapsedFamily(treeId)}
|
||||
hotkeys={hotkeys}
|
||||
getItemKey={getItemKey}
|
||||
ItemInner={SidebarInnerItem}
|
||||
ItemLeftSlotInner={SidebarLeftSlot}
|
||||
|
||||
31
apps/yaak-proxy/ActionButton.tsx
Normal file
31
apps/yaak-proxy/ActionButton.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Button, type ButtonProps } from "@yaakapp-internal/ui";
|
||||
import { useCallback, useState } from "react";
|
||||
import type { ActionInvocation } from "@yaakapp-internal/proxy-lib";
|
||||
import { useActionMetadata } from "./hooks";
|
||||
import { rpc } from "./rpc";
|
||||
|
||||
type ActionButtonProps = Omit<ButtonProps, "onClick" | "children"> & {
|
||||
action: ActionInvocation;
|
||||
/** Override the label from metadata */
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function ActionButton({ action, children, ...props }: ActionButtonProps) {
|
||||
const meta = useActionMetadata(action);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await rpc("execute_action", action);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [action]);
|
||||
|
||||
return (
|
||||
<Button {...props} disabled={props.disabled || busy} isLoading={busy} onClick={onClick}>
|
||||
{children ?? meta?.label ?? "…"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
35
apps/yaak-proxy/hooks.ts
Normal file
35
apps/yaak-proxy/hooks.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type {
|
||||
ActionInvocation,
|
||||
ActionMetadata,
|
||||
} from "@yaakapp-internal/proxy-lib";
|
||||
import { rpc } from "./rpc";
|
||||
|
||||
let cachedActions: [ActionInvocation, ActionMetadata][] | null = null;
|
||||
|
||||
/** Fetch and cache all action metadata. */
|
||||
async function getActions(): Promise<[ActionInvocation, ActionMetadata][]> {
|
||||
if (!cachedActions) {
|
||||
const { actions } = await rpc("list_actions", {});
|
||||
cachedActions = actions;
|
||||
}
|
||||
return cachedActions;
|
||||
}
|
||||
|
||||
/** Look up metadata for a specific action invocation. */
|
||||
export function useActionMetadata(
|
||||
action: ActionInvocation,
|
||||
): ActionMetadata | null {
|
||||
const [meta, setMeta] = useState<ActionMetadata | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getActions().then((actions) => {
|
||||
const match = actions.find(
|
||||
([inv]) => inv.scope === action.scope && inv.action === action.action,
|
||||
);
|
||||
setMeta(match?.[1] ?? null);
|
||||
});
|
||||
}, [action]);
|
||||
|
||||
return meta;
|
||||
}
|
||||
64
apps/yaak-proxy/hotkeys.ts
Normal file
64
apps/yaak-proxy/hotkeys.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type {
|
||||
ActionInvocation,
|
||||
ActionMetadata,
|
||||
} from "@yaakapp-internal/proxy-lib";
|
||||
import { rpc } from "./rpc";
|
||||
|
||||
type ActionBinding = {
|
||||
invocation: ActionInvocation;
|
||||
meta: ActionMetadata;
|
||||
keys: { key: string; ctrl: boolean; shift: boolean; alt: boolean; meta: boolean };
|
||||
};
|
||||
|
||||
/** Parse a hotkey string like "Ctrl+Shift+P" into its parts. */
|
||||
function parseHotkey(hotkey: string): ActionBinding["keys"] {
|
||||
const parts = hotkey.split("+").map((p) => p.trim().toLowerCase());
|
||||
return {
|
||||
ctrl: parts.includes("ctrl") || parts.includes("control"),
|
||||
shift: parts.includes("shift"),
|
||||
alt: parts.includes("alt"),
|
||||
meta: parts.includes("meta") || parts.includes("cmd") || parts.includes("command"),
|
||||
key: parts.filter(
|
||||
(p) => !["ctrl", "control", "shift", "alt", "meta", "cmd", "command"].includes(p),
|
||||
)[0] ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
function matchesEvent(binding: ActionBinding["keys"], e: KeyboardEvent): boolean {
|
||||
return (
|
||||
e.ctrlKey === binding.ctrl &&
|
||||
e.shiftKey === binding.shift &&
|
||||
e.altKey === binding.alt &&
|
||||
e.metaKey === binding.meta &&
|
||||
e.key.toLowerCase() === binding.key
|
||||
);
|
||||
}
|
||||
|
||||
/** Fetch all actions from Rust and register a global keydown listener. */
|
||||
export async function initHotkeys(): Promise<() => void> {
|
||||
const { actions } = await rpc("list_actions", {});
|
||||
|
||||
const bindings: ActionBinding[] = actions
|
||||
.filter(
|
||||
(entry): entry is [ActionInvocation, ActionMetadata & { defaultHotkey: string }] =>
|
||||
entry[1].defaultHotkey != null,
|
||||
)
|
||||
.map(([invocation, meta]) => ({
|
||||
invocation,
|
||||
meta,
|
||||
keys: parseHotkey(meta.defaultHotkey),
|
||||
}));
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
for (const binding of bindings) {
|
||||
if (matchesEvent(binding.keys, e)) {
|
||||
e.preventDefault();
|
||||
rpc("execute_action", binding.invocation);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { type } from "@tauri-apps/plugin-os";
|
||||
import { Button, HeaderSize } from "@yaakapp-internal/ui";
|
||||
import { HeaderSize } from "@yaakapp-internal/ui";
|
||||
import { ActionButton } from "./ActionButton";
|
||||
import classNames from "classnames";
|
||||
import { createStore, Provider, useAtomValue } from "jotai";
|
||||
import { StrictMode, useState } from "react";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./main.css";
|
||||
import { initHotkeys } from "./hotkeys";
|
||||
import { listen, rpc } from "./rpc";
|
||||
import { applyChange, dataAtom, httpExchangesAtom, replaceAll } from "./store";
|
||||
|
||||
@@ -19,6 +21,9 @@ rpc("list_models", {}).then((res) => {
|
||||
);
|
||||
});
|
||||
|
||||
// Register hotkeys from action metadata
|
||||
initHotkeys();
|
||||
|
||||
// Subscribe to model change events from the backend
|
||||
listen("model_write", (payload) => {
|
||||
jotaiStore.set(dataAtom, (prev) =>
|
||||
@@ -27,37 +32,9 @@ listen("model_write", (payload) => {
|
||||
});
|
||||
|
||||
function App() {
|
||||
const [status, setStatus] = useState("Idle");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const osType = type();
|
||||
const exchanges = useAtomValue(httpExchangesAtom);
|
||||
|
||||
async function startProxy() {
|
||||
setBusy(true);
|
||||
setStatus("Starting...");
|
||||
try {
|
||||
await rpc("execute_action", { scope: "global", action: "proxy_start" });
|
||||
setStatus("Running");
|
||||
} catch (err) {
|
||||
setStatus(`Failed: ${String(err)}`);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function stopProxy() {
|
||||
setBusy(true);
|
||||
setStatus("Stopping...");
|
||||
try {
|
||||
await rpc("execute_action", { scope: "global", action: "proxy_stop" });
|
||||
setStatus("Stopped");
|
||||
} catch (err) {
|
||||
setStatus(`Failed: ${String(err)}`);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -82,20 +59,16 @@ function App() {
|
||||
</HeaderSize>
|
||||
<main className="overflow-auto p-4">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Button disabled={busy} onClick={startProxy} size="sm" tone="primary">
|
||||
Start Proxy
|
||||
</Button>
|
||||
<Button
|
||||
disabled={busy}
|
||||
onClick={stopProxy}
|
||||
<ActionButton
|
||||
action={{ scope: "global", action: "proxy_start" }}
|
||||
size="sm"
|
||||
tone="primary"
|
||||
/>
|
||||
<ActionButton
|
||||
action={{ scope: "global", action: "proxy_stop" }}
|
||||
size="sm"
|
||||
variant="border"
|
||||
>
|
||||
Stop Proxy
|
||||
</Button>
|
||||
<span className="text-xs text-text-subtlest">
|
||||
{status}
|
||||
</span>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-xs font-mono">
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@tauri-apps/plugin-os": "^2.3.2",
|
||||
"@yaakapp-internal/theme": "^1.0.0",
|
||||
"@yaakapp-internal/model-store": "^1.0.0",
|
||||
"@yaakapp-internal/proxy-lib": "^1.0.0",
|
||||
"@yaakapp-internal/ui": "^1.0.0",
|
||||
"classnames": "^2.5.1",
|
||||
"jotai": "^2.18.0",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { listen as tauriListen } from "@tauri-apps/api/event";
|
||||
import type {
|
||||
RpcEventSchema,
|
||||
RpcSchema,
|
||||
} from "../../crates-proxy/yaak-proxy-lib/bindings/gen_rpc";
|
||||
} from "@yaakapp-internal/proxy-lib";
|
||||
|
||||
type Req<K extends keyof RpcSchema> = RpcSchema[K][0];
|
||||
type Res<K extends keyof RpcSchema> = RpcSchema[K][1];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createModelStore } from "@yaakapp-internal/model-store";
|
||||
import type { HttpExchange } from "../../crates-proxy/yaak-proxy-lib/bindings/gen_models";
|
||||
import type { HttpExchange } from "@yaakapp-internal/proxy-lib";
|
||||
|
||||
type ProxyModels = {
|
||||
http_exchange: HttpExchange;
|
||||
|
||||
18
crates-proxy/yaak-proxy-lib/bindings/gen_rpc.ts
generated
18
crates-proxy/yaak-proxy-lib/bindings/gen_rpc.ts
generated
@@ -1,16 +1,20 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { HttpExchange, ModelPayload } from "./gen_models";
|
||||
|
||||
export type ActionInvocation = { "scope": "global", action: GlobalAction, };
|
||||
|
||||
export type ActionMetadata = { label: string, defaultHotkey: string | null, };
|
||||
|
||||
export type GlobalAction = "proxy_start" | "proxy_stop";
|
||||
|
||||
export type ListActionsRequest = Record<string, never>;
|
||||
|
||||
export type ListActionsResponse = { actions: Array<[ActionInvocation, ActionMetadata]>, };
|
||||
|
||||
export type ListModelsRequest = Record<string, never>;
|
||||
|
||||
export type ListModelsResponse = { httpExchanges: Array<HttpExchange>, };
|
||||
|
||||
export type ProxyStartRequest = { port: number | null, };
|
||||
|
||||
export type ProxyStartResponse = { port: number, alreadyRunning: boolean, };
|
||||
|
||||
export type ProxyStopRequest = Record<string, never>;
|
||||
|
||||
export type RpcEventSchema = { model_write: ModelPayload, };
|
||||
|
||||
export type RpcSchema = { proxy_start: [ProxyStartRequest, ProxyStartResponse], proxy_stop: [ProxyStopRequest, boolean], list_models: [ListModelsRequest, ListModelsResponse], };
|
||||
export type RpcSchema = { execute_action: [ActionInvocation, boolean], list_actions: [ListActionsRequest, ListActionsResponse], list_models: [ListModelsRequest, ListModelsResponse], };
|
||||
|
||||
2
crates-proxy/yaak-proxy-lib/bindings/index.ts
generated
Normal file
2
crates-proxy/yaak-proxy-lib/bindings/index.ts
generated
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./gen_rpc";
|
||||
export * from "./gen_models";
|
||||
6
crates-proxy/yaak-proxy-lib/package.json
Normal file
6
crates-proxy/yaak-proxy-lib/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@yaakapp-internal/proxy-lib",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"main": "bindings/index.ts"
|
||||
}
|
||||
@@ -15,3 +15,39 @@ pub enum GlobalAction {
|
||||
pub enum ActionInvocation {
|
||||
Global { action: GlobalAction },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub struct ActionMetadata {
|
||||
pub label: String,
|
||||
pub default_hotkey: Option<String>,
|
||||
}
|
||||
|
||||
fn default_hotkey(mac: &str, other: &str) -> Option<String> {
|
||||
if cfg!(target_os = "macos") {
|
||||
Some(mac.into())
|
||||
} else {
|
||||
Some(other.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// All global actions with their metadata, used by `list_actions` RPC.
|
||||
pub fn all_global_actions() -> Vec<(ActionInvocation, ActionMetadata)> {
|
||||
vec![
|
||||
(
|
||||
ActionInvocation::Global { action: GlobalAction::ProxyStart },
|
||||
ActionMetadata {
|
||||
label: "Start Proxy".into(),
|
||||
default_hotkey: default_hotkey("Meta+Shift+P", "Ctrl+Shift+P"),
|
||||
},
|
||||
),
|
||||
(
|
||||
ActionInvocation::Global { action: GlobalAction::ProxyStop },
|
||||
ActionMetadata {
|
||||
label: "Stop Proxy".into(),
|
||||
default_hotkey: default_hotkey("Meta+Shift+S", "Ctrl+Shift+S"),
|
||||
},
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use ts_rs::TS;
|
||||
use yaak_database::{ModelChangeEvent, UpdateSource};
|
||||
use yaak_proxy::{CapturedRequest, ProxyEvent, ProxyHandle, RequestState};
|
||||
use yaak_rpc::{RpcError, RpcEventEmitter, define_rpc};
|
||||
use crate::actions::{ActionInvocation, GlobalAction};
|
||||
use crate::actions::{ActionInvocation, ActionMetadata, GlobalAction};
|
||||
use crate::db::ProxyQueryManager;
|
||||
use crate::models::{HttpExchange, ModelPayload, ProxyHeader};
|
||||
|
||||
@@ -35,6 +35,16 @@ impl ProxyCtx {
|
||||
|
||||
// -- Request/response types --
|
||||
|
||||
#[derive(Deserialize, TS)]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub struct ListActionsRequest {}
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub struct ListActionsResponse {
|
||||
pub actions: Vec<(ActionInvocation, ActionMetadata)>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, TS)]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub struct ListModelsRequest {}
|
||||
@@ -85,6 +95,12 @@ fn execute_action(ctx: &ProxyCtx, invocation: ActionInvocation) -> Result<bool,
|
||||
}
|
||||
}
|
||||
|
||||
fn list_actions(_ctx: &ProxyCtx, _req: ListActionsRequest) -> Result<ListActionsResponse, RpcError> {
|
||||
Ok(ListActionsResponse {
|
||||
actions: crate::actions::all_global_actions(),
|
||||
})
|
||||
}
|
||||
|
||||
fn list_models(ctx: &ProxyCtx, _req: ListModelsRequest) -> Result<ListModelsResponse, RpcError> {
|
||||
ctx.db.with_conn(|db| {
|
||||
Ok(ListModelsResponse {
|
||||
@@ -200,6 +216,7 @@ define_rpc! {
|
||||
ProxyCtx;
|
||||
commands {
|
||||
execute_action(ActionInvocation) -> bool,
|
||||
list_actions(ListActionsRequest) -> ListActionsResponse,
|
||||
list_models(ListModelsRequest) -> ListModelsResponse,
|
||||
}
|
||||
events {
|
||||
|
||||
3
crates/yaak-models/bindings/ModelChangeEvent.ts
generated
Normal file
3
crates/yaak-models/bindings/ModelChangeEvent.ts
generated
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ModelChangeEvent = { "type": "upsert", created: boolean, } | { "type": "delete" };
|
||||
39
crates/yaak-models/bindings/gen_models.ts
generated
39
crates/yaak-models/bindings/gen_models.ts
generated
@@ -1,4 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ModelChangeEvent } from "./ModelChangeEvent";
|
||||
|
||||
export type AnyModel = CookieJar | Environment | Folder | GraphQlIntrospection | GrpcConnection | GrpcEvent | GrpcRequest | HttpRequest | HttpResponse | HttpResponseEvent | KeyValue | Plugin | Settings | SyncState | WebsocketConnection | WebsocketEvent | WebsocketRequest | Workspace | WorkspaceMeta;
|
||||
|
||||
@@ -18,7 +19,12 @@ export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
||||
|
||||
export type EncryptedKey = { encryptedKey: string, };
|
||||
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null,
|
||||
/**
|
||||
* Variables defined in this environment scope.
|
||||
* Child environments override parent variables by name.
|
||||
*/
|
||||
variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
||||
|
||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
@@ -34,9 +40,17 @@ export type GrpcEvent = { model: "grpc_event", id: string, createdAt: string, up
|
||||
|
||||
export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end";
|
||||
|
||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
|
||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number,
|
||||
/**
|
||||
* Server URL (http for plaintext or https for secure)
|
||||
*/
|
||||
url: string, };
|
||||
|
||||
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string,
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>, };
|
||||
|
||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
@@ -55,12 +69,15 @@ export type HttpResponseHeader = { name: string, value: string, };
|
||||
|
||||
export type HttpResponseState = "initialized" | "connected" | "closed";
|
||||
|
||||
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
export type HttpUrlParameter = { enabled?: boolean,
|
||||
/**
|
||||
* Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
|
||||
* Other entries are appended as query parameters
|
||||
*/
|
||||
name: string, value: string, id?: string, };
|
||||
|
||||
export type KeyValue = { model: "key_value", id: string, createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
|
||||
|
||||
export type ModelChangeEvent = { "type": "upsert", created: boolean, } | { "type": "delete" };
|
||||
|
||||
export type ModelPayload = { model: AnyModel, updateSource: UpdateSource, change: ModelChangeEvent, };
|
||||
|
||||
export type ParentAuthentication = { authentication: Record<string, any>, authenticationType: string | null, };
|
||||
@@ -69,10 +86,10 @@ export type ParentHeaders = { headers: Array<HttpRequestHeader>, };
|
||||
|
||||
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, source: PluginSource, };
|
||||
|
||||
export type PluginSource = "bundled" | "filesystem" | "registry";
|
||||
|
||||
export type PluginKeyValue = { model: "plugin_key_value", createdAt: string, updatedAt: string, pluginName: string, key: string, value: string, };
|
||||
|
||||
export type PluginSource = "bundled" | "filesystem" | "registry";
|
||||
|
||||
export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" };
|
||||
|
||||
export type ProxySettingAuth = { user: string, password: string, };
|
||||
@@ -93,7 +110,11 @@ export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping"
|
||||
|
||||
export type WebsocketMessageType = "text" | "binary";
|
||||
|
||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string,
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>, };
|
||||
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ mod tests {
|
||||
let changes = db.list_model_changes_after(0, 10).expect("Failed to list changes");
|
||||
assert_eq!(changes.len(), 1);
|
||||
|
||||
db.conn
|
||||
db.conn()
|
||||
.resolve()
|
||||
.execute(
|
||||
"UPDATE model_changes SET created_at = '2000-01-01 00:00:00.000' WHERE id = ?1",
|
||||
@@ -199,7 +199,7 @@ mod tests {
|
||||
assert_eq!(all.len(), 2);
|
||||
|
||||
let fixed_ts = "2026-02-16 00:00:00.000";
|
||||
db.conn
|
||||
db.conn()
|
||||
.resolve()
|
||||
.execute("UPDATE model_changes SET created_at = ?1", params![fixed_ts])
|
||||
.expect("Failed to normalize timestamps");
|
||||
@@ -229,7 +229,7 @@ mod tests {
|
||||
let changes = db.list_model_changes_after(0, 10).expect("Failed to list changes");
|
||||
assert_eq!(changes.len(), 1);
|
||||
|
||||
db.conn
|
||||
db.conn()
|
||||
.resolve()
|
||||
.execute(
|
||||
"UPDATE model_changes SET created_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW', '-2 hours') WHERE id = ?1",
|
||||
@@ -264,7 +264,7 @@ mod tests {
|
||||
"change": { "type": "upsert", "created": false }
|
||||
});
|
||||
|
||||
db.conn
|
||||
db.conn()
|
||||
.resolve()
|
||||
.execute(
|
||||
r#"
|
||||
|
||||
91
package-lock.json
generated
91
package-lock.json
generated
@@ -66,6 +66,7 @@
|
||||
"crates/yaak-sync",
|
||||
"crates/yaak-templates",
|
||||
"crates/yaak-ws",
|
||||
"crates-proxy/yaak-proxy-lib",
|
||||
"apps/yaak-client",
|
||||
"apps/yaak-proxy"
|
||||
],
|
||||
@@ -77,7 +78,7 @@
|
||||
"@codemirror/legacy-modes": "^6.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.13",
|
||||
"@biomejs/biome": "^2.4.6",
|
||||
"@tauri-apps/cli": "^2.9.6",
|
||||
"@yaakapp/cli": "^0.5.1",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
@@ -230,6 +231,7 @@
|
||||
"@tauri-apps/api": "^2.9.1",
|
||||
"@tauri-apps/plugin-os": "^2.3.2",
|
||||
"@yaakapp-internal/model-store": "^1.0.0",
|
||||
"@yaakapp-internal/proxy-lib": "^1.0.0",
|
||||
"@yaakapp-internal/theme": "^1.0.0",
|
||||
"@yaakapp-internal/ui": "^1.0.0",
|
||||
"classnames": "^2.5.1",
|
||||
@@ -245,6 +247,10 @@
|
||||
"vite": "^7.0.8"
|
||||
}
|
||||
},
|
||||
"crates-proxy/yaak-proxy-lib": {
|
||||
"name": "@yaakapp-internal/proxy-lib",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"crates-tauri/yaak-app-client": {
|
||||
"name": "@yaakapp-internal/tauri-client",
|
||||
"version": "1.0.0"
|
||||
@@ -675,11 +681,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/biome": {
|
||||
"version": "2.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.13.tgz",
|
||||
"integrity": "sha512-Fw7UsV0UAtWIBIm0M7g5CRerpu1eKyKAXIazzxhbXYUyMkwNrkX/KLkGI7b+uVDQ5cLUMfOC9vR60q9IDYDstA==",
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.6.tgz",
|
||||
"integrity": "sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"bin": {
|
||||
"biome": "bin/biome"
|
||||
},
|
||||
@@ -691,25 +696,24 @@
|
||||
"url": "https://opencollective.com/biome"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@biomejs/cli-darwin-arm64": "2.3.13",
|
||||
"@biomejs/cli-darwin-x64": "2.3.13",
|
||||
"@biomejs/cli-linux-arm64": "2.3.13",
|
||||
"@biomejs/cli-linux-arm64-musl": "2.3.13",
|
||||
"@biomejs/cli-linux-x64": "2.3.13",
|
||||
"@biomejs/cli-linux-x64-musl": "2.3.13",
|
||||
"@biomejs/cli-win32-arm64": "2.3.13",
|
||||
"@biomejs/cli-win32-x64": "2.3.13"
|
||||
"@biomejs/cli-darwin-arm64": "2.4.6",
|
||||
"@biomejs/cli-darwin-x64": "2.4.6",
|
||||
"@biomejs/cli-linux-arm64": "2.4.6",
|
||||
"@biomejs/cli-linux-arm64-musl": "2.4.6",
|
||||
"@biomejs/cli-linux-x64": "2.4.6",
|
||||
"@biomejs/cli-linux-x64-musl": "2.4.6",
|
||||
"@biomejs/cli-win32-arm64": "2.4.6",
|
||||
"@biomejs/cli-win32-x64": "2.4.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-darwin-arm64": {
|
||||
"version": "2.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.13.tgz",
|
||||
"integrity": "sha512-0OCwP0/BoKzyJHnFdaTk/i7hIP9JHH9oJJq6hrSCPmJPo8JWcJhprK4gQlhFzrwdTBAW4Bjt/RmCf3ZZe59gwQ==",
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.6.tgz",
|
||||
"integrity": "sha512-NW18GSyxr+8sJIqgoGwVp5Zqm4SALH4b4gftIA0n62PTuBs6G2tHlwNAOj0Vq0KKSs7Sf88VjjmHh0O36EnzrQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
@@ -719,14 +723,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-darwin-x64": {
|
||||
"version": "2.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.13.tgz",
|
||||
"integrity": "sha512-AGr8OoemT/ejynbIu56qeil2+F2WLkIjn2d8jGK1JkchxnMUhYOfnqc9sVzcRxpG9Ycvw4weQ5sprRvtb7Yhcw==",
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.6.tgz",
|
||||
"integrity": "sha512-4uiE/9tuI7cnjtY9b07RgS7gGyYOAfIAGeVJWEfeCnAarOAS7qVmuRyX6d7JTKw28/mt+rUzMasYeZ+0R/U1Mw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
@@ -736,14 +739,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-linux-arm64": {
|
||||
"version": "2.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.13.tgz",
|
||||
"integrity": "sha512-xvOiFkrDNu607MPMBUQ6huHmBG1PZLOrqhtK6pXJW3GjfVqJg0Z/qpTdhXfcqWdSZHcT+Nct2fOgewZvytESkw==",
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.6.tgz",
|
||||
"integrity": "sha512-kMLaI7OF5GN1Q8Doymjro1P8rVEoy7BKQALNz6fiR8IC1WKduoNyteBtJlHT7ASIL0Cx2jR6VUOBIbcB1B8pew==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -753,14 +755,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
||||
"version": "2.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.13.tgz",
|
||||
"integrity": "sha512-TUdDCSY+Eo/EHjhJz7P2GnWwfqet+lFxBZzGHldrvULr59AgahamLs/N85SC4+bdF86EhqDuuw9rYLvLFWWlXA==",
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.6.tgz",
|
||||
"integrity": "sha512-F/JdB7eN22txiTqHM5KhIVt0jVkzZwVYrdTR1O3Y4auBOQcXxHK4dxULf4z43QyZI5tsnQJrRBHZy7wwtL+B3A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -770,14 +771,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-linux-x64": {
|
||||
"version": "2.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.13.tgz",
|
||||
"integrity": "sha512-s+YsZlgiXNq8XkgHs6xdvKDFOj/bwTEevqEY6rC2I3cBHbxXYU1LOZstH3Ffw9hE5tE1sqT7U23C00MzkXztMw==",
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.6.tgz",
|
||||
"integrity": "sha512-oHXmUFEoH8Lql1xfc3QkFLiC1hGR7qedv5eKNlC185or+o4/4HiaU7vYODAH3peRCfsuLr1g6v2fK9dFFOYdyw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -787,14 +787,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-linux-x64-musl": {
|
||||
"version": "2.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.13.tgz",
|
||||
"integrity": "sha512-0bdwFVSbbM//Sds6OjtnmQGp4eUjOTt6kHvR/1P0ieR9GcTUAlPNvPC3DiavTqq302W34Ae2T6u5VVNGuQtGlQ==",
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.6.tgz",
|
||||
"integrity": "sha512-C9s98IPDu7DYarjlZNuzJKTjVHN03RUnmHV5htvqsx6vEUXCDSJ59DNwjKVD5XYoSS4N+BYhq3RTBAL8X6svEg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -804,14 +803,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-win32-arm64": {
|
||||
"version": "2.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.13.tgz",
|
||||
"integrity": "sha512-QweDxY89fq0VvrxME+wS/BXKmqMrOTZlN9SqQ79kQSIc3FrEwvW/PvUegQF6XIVaekncDykB5dzPqjbwSKs9DA==",
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.6.tgz",
|
||||
"integrity": "sha512-xzThn87Pf3YrOGTEODFGONmqXpTwUNxovQb72iaUOdcw8sBSY3+3WD8Hm9IhMYLnPi0n32s3L3NWU6+eSjfqFg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -821,14 +819,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-win32-x64": {
|
||||
"version": "2.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.13.tgz",
|
||||
"integrity": "sha512-trDw2ogdM2lyav9WFQsdsfdVy1dvZALymRpgmWsvSez0BJzBjulhOT/t+wyKeh3pZWvwP3VMs1SoOKwO3wecMQ==",
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.6.tgz",
|
||||
"integrity": "sha512-7++XhnsPlr1HDbor5amovPjOH6vsrFOCdp93iKXhFn6bcMUI6soodj3WWKfgEO6JosKU1W5n3uky3WW9RlRjTg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -4445,6 +4442,10 @@
|
||||
"resolved": "crates/yaak-plugins",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@yaakapp-internal/proxy-lib": {
|
||||
"resolved": "crates-proxy/yaak-proxy-lib",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@yaakapp-internal/sse": {
|
||||
"resolved": "crates/yaak-sse",
|
||||
"link": true
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"crates/yaak-sync",
|
||||
"crates/yaak-templates",
|
||||
"crates/yaak-ws",
|
||||
"crates-proxy/yaak-proxy-lib",
|
||||
"apps/yaak-client",
|
||||
"apps/yaak-proxy"
|
||||
],
|
||||
@@ -105,7 +106,7 @@
|
||||
"js-yaml": "^4.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.13",
|
||||
"@biomejs/biome": "^2.4.6",
|
||||
"@tauri-apps/cli": "^2.9.6",
|
||||
"@yaakapp/cli": "^0.5.1",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
|
||||
@@ -22,9 +22,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useKey, useKeyPressEvent } from 'react-use';
|
||||
import type { HotKeyOptions, HotkeyAction } from '../../../hooks/useHotKey';
|
||||
import { useHotKey } from '../../../hooks/useHotKey';
|
||||
import { computeSideForDragMove } from '@yaakapp-internal/ui';
|
||||
import { computeSideForDragMove } from '../../lib/dnd';
|
||||
import { useStore } from 'jotai';
|
||||
import {
|
||||
draggingIdsFamily,
|
||||
@@ -57,9 +55,6 @@ export interface TreeProps<T extends { id: string }> {
|
||||
className?: string;
|
||||
onActivate?: (item: T) => void;
|
||||
onDragEnd?: (opt: { items: T[]; parent: T; children: T[]; insertAt: number }) => void;
|
||||
hotkeys?: {
|
||||
actions: Partial<Record<HotkeyAction, { cb: (items: T[]) => void } & HotKeyOptions>>;
|
||||
};
|
||||
getEditOptions?: (item: T) => {
|
||||
defaultValue: string;
|
||||
placeholder?: string;
|
||||
@@ -71,6 +66,7 @@ export interface TreeHandle {
|
||||
treeId: string;
|
||||
focus: () => boolean;
|
||||
hasFocus: () => boolean;
|
||||
getSelectedItems: () => { id: string }[];
|
||||
selectItem: (id: string, focus?: boolean) => void;
|
||||
renameItem: (id: string) => void;
|
||||
showContextMenu: () => void;
|
||||
@@ -83,7 +79,6 @@ function TreeInner<T extends { id: string }>(
|
||||
getContextMenu,
|
||||
getEditOptions,
|
||||
getItemKey,
|
||||
hotkeys,
|
||||
onActivate,
|
||||
onDragEnd,
|
||||
renderContextMenu,
|
||||
@@ -202,6 +197,7 @@ function TreeInner<T extends { id: string }>(
|
||||
treeId,
|
||||
focus: tryFocus,
|
||||
hasFocus: hasFocus,
|
||||
getSelectedItems: () => getSelectedItems(store, treeId, selectableItems),
|
||||
renameItem: (id) => treeItemRefs.current[id]?.rename(),
|
||||
selectItem: (id, focus) => {
|
||||
if (store.get(selectedIdsFamily(treeId)).includes(id)) {
|
||||
@@ -650,7 +646,6 @@ function TreeInner<T extends { id: string }>(
|
||||
|
||||
return (
|
||||
<CollapsedAtomContext.Provider value={collapsedAtom}>
|
||||
<TreeHotKeys treeId={treeId} hotkeys={hotkeys} selectableItems={selectableItems} />
|
||||
{showContextMenu &&
|
||||
renderContextMenu?.({
|
||||
items: showContextMenu.items,
|
||||
@@ -742,68 +737,6 @@ function DropRegionAfterList({
|
||||
return <div ref={setNodeRef} onContextMenu={onContextMenu} />;
|
||||
}
|
||||
|
||||
interface TreeHotKeyProps<T extends { id: string }> {
|
||||
action: HotkeyAction;
|
||||
selectableItems: SelectableTreeNode<T>[];
|
||||
treeId: string;
|
||||
onDone: (items: T[]) => void;
|
||||
priority?: number;
|
||||
enable?: boolean | (() => boolean);
|
||||
}
|
||||
|
||||
function TreeHotKey<T extends { id: string }>({
|
||||
treeId,
|
||||
action,
|
||||
onDone,
|
||||
selectableItems,
|
||||
enable,
|
||||
...options
|
||||
}: TreeHotKeyProps<T>) {
|
||||
const store = useStore();
|
||||
useHotKey(
|
||||
action,
|
||||
() => {
|
||||
onDone(getSelectedItems(store, treeId, selectableItems));
|
||||
},
|
||||
{
|
||||
...options,
|
||||
enable: () => {
|
||||
if (enable == null) return true;
|
||||
if (typeof enable === 'function') return enable();
|
||||
return enable;
|
||||
},
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
function TreeHotKeys<T extends { id: string }>({
|
||||
treeId,
|
||||
hotkeys,
|
||||
selectableItems,
|
||||
}: {
|
||||
treeId: string;
|
||||
hotkeys: TreeProps<T>['hotkeys'];
|
||||
selectableItems: SelectableTreeNode<T>[];
|
||||
}) {
|
||||
if (hotkeys == null) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.entries(hotkeys.actions).map(([hotkey, { cb, ...options }]) => (
|
||||
<TreeHotKey
|
||||
key={hotkey}
|
||||
action={hotkey as HotkeyAction}
|
||||
treeId={treeId}
|
||||
onDone={cb}
|
||||
selectableItems={selectableItems}
|
||||
{...options}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getValidSelectableItems<T extends { id: string }>(
|
||||
store: JotaiStore,
|
||||
collapsedAtom: CollapsedAtom,
|
||||
@@ -1,7 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { memo } from 'react';
|
||||
import { DropMarker } from '@yaakapp-internal/ui';
|
||||
import { DropMarker } from '../DropMarker';
|
||||
import { hoveredParentDepthFamily, isIndexHoveredFamily } from './atoms';
|
||||
import type { TreeNode } from './common';
|
||||
import { useIsCollapsed } from './context';
|
||||
@@ -9,9 +9,9 @@ import type {
|
||||
KeyboardEvent as ReactKeyboardEvent,
|
||||
} from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { computeSideForDragMove } from '@yaakapp-internal/ui';
|
||||
import { computeSideForDragMove } from '../../lib/dnd';
|
||||
import type { ContextMenuRenderer } from './common';
|
||||
import { Icon } from '@yaakapp-internal/ui';
|
||||
import { Icon } from '../Icon';
|
||||
import { isLastFocusedFamily, isSelectedFamily } from './atoms';
|
||||
import { useCollapsedAtom, useIsAncestorCollapsed, useIsCollapsed, useSetCollapsed } from './context';
|
||||
import type { TreeNode } from './common';
|
||||
@@ -11,3 +11,8 @@ export { useDebouncedState } from "./hooks/useDebouncedState";
|
||||
export { HEADER_SIZE_MD, HEADER_SIZE_LG, WINDOW_CONTROLS_WIDTH } from "./lib/constants";
|
||||
export { DropMarker } from "./components/DropMarker";
|
||||
export { computeSideForDragMove } from "./lib/dnd";
|
||||
export { Tree } from "./components/tree/Tree";
|
||||
export type { TreeHandle, TreeProps } from "./components/tree/Tree";
|
||||
export type { TreeNode } from "./components/tree/common";
|
||||
export type { TreeItemProps } from "./components/tree/TreeItem";
|
||||
export { isSelectedFamily, selectedIdsFamily } from "./components/tree/atoms";
|
||||
|
||||
Reference in New Issue
Block a user