mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-05-18 05:37:09 +02:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c0835031b | |||
| 0a6aed6cc6 | |||
| 0c97036864 | |||
| 1a705ff244 | |||
| dc47b54b1c | |||
| dcfdf077e7 | |||
| bde5a474cc | |||
| 21f1dad7a4 | |||
| 6dac1265f3 | |||
| 77ab293f87 | |||
| 471a099b9b | |||
| b0b282535f | |||
| 19ed8c2f0d | |||
| d7e67cf13c | |||
| 1b154ba550 | |||
| 10559c8f4f | |||
| d2dc719cc6 | |||
| 50f33b45b9 |
+7
-7
@@ -8,7 +8,7 @@ Make Yaak runnable as a standalone CLI without Tauri as a dependency. The core R
|
|||||||
|
|
||||||
```
|
```
|
||||||
crates/ # Core crates - should NOT depend on Tauri
|
crates/ # Core crates - should NOT depend on Tauri
|
||||||
crates-tauri/ # Tauri-specific crates (yaak-app, yaak-tauri-utils, etc.)
|
crates-tauri/ # Tauri-specific crates (yaak-app-client, yaak-tauri-utils, etc.)
|
||||||
crates-cli/ # CLI crate (yaak-cli)
|
crates-cli/ # CLI crate (yaak-cli)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ crates-cli/ # CLI crate (yaak-cli)
|
|||||||
|
|
||||||
### 1. Folder Restructure
|
### 1. Folder Restructure
|
||||||
|
|
||||||
- Moved Tauri-dependent app code to `crates-tauri/yaak-app/`
|
- Moved Tauri-dependent app code to `crates-tauri/yaak-app-client/`
|
||||||
- Created `crates-tauri/yaak-tauri-utils/` for shared Tauri utilities (window traits, api_client, error handling)
|
- Created `crates-tauri/yaak-tauri-utils/` for shared Tauri utilities (window traits, api_client, error handling)
|
||||||
- Created `crates-cli/yaak-cli/` for the standalone CLI
|
- Created `crates-cli/yaak-cli/` for the standalone CLI
|
||||||
|
|
||||||
@@ -50,14 +50,14 @@ crates-cli/ # CLI crate (yaak-cli)
|
|||||||
3. Move extension traits (e.g., `SomethingManagerExt`) to yaak-app or yaak-tauri-utils
|
3. Move extension traits (e.g., `SomethingManagerExt`) to yaak-app or yaak-tauri-utils
|
||||||
4. Initialize managers in yaak-app's `.setup()` block
|
4. Initialize managers in yaak-app's `.setup()` block
|
||||||
5. Remove `tauri` from Cargo.toml dependencies
|
5. Remove `tauri` from Cargo.toml dependencies
|
||||||
6. Update `crates-tauri/yaak-app/capabilities/default.json` to remove the plugin permission
|
6. Update `crates-tauri/yaak-app-client/capabilities/default.json` to remove the plugin permission
|
||||||
7. Replace `tauri::async_runtime::block_on` with `tokio::runtime::Handle::current().block_on()`
|
7. Replace `tauri::async_runtime::block_on` with `tokio::runtime::Handle::current().block_on()`
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
- `crates-tauri/yaak-app/src/lib.rs` - Main Tauri app, setup block initializes managers
|
- `crates-tauri/yaak-app-client/src/lib.rs` - Main Tauri app, setup block initializes managers
|
||||||
- `crates-tauri/yaak-app/src/commands.rs` - Migrated Tauri commands
|
- `crates-tauri/yaak-app-client/src/commands.rs` - Migrated Tauri commands
|
||||||
- `crates-tauri/yaak-app/src/models_ext.rs` - Database plugin and extension traits
|
- `crates-tauri/yaak-app-client/src/models_ext.rs` - Database plugin and extension traits
|
||||||
- `crates-tauri/yaak-tauri-utils/src/window.rs` - WorkspaceWindowTrait for window state
|
- `crates-tauri/yaak-tauri-utils/src/window.rs` - WorkspaceWindowTrait for window state
|
||||||
- `crates/yaak-models/src/lib.rs` - Contains `init_standalone()` for CLI usage
|
- `crates/yaak-models/src/lib.rs` - Contains `init_standalone()` for CLI usage
|
||||||
|
|
||||||
@@ -79,5 +79,5 @@ e718a5f1 Refactor models_ext to use init_standalone from yaak-models
|
|||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- Run `cargo check -p <crate>` to verify a crate builds without Tauri
|
- Run `cargo check -p <crate>` to verify a crate builds without Tauri
|
||||||
- Run `npm run app-dev` to test the Tauri app still works
|
- Run `npm run client:dev` to test the Tauri app still works
|
||||||
- Run `cargo run -p yaak-cli -- --help` to test the CLI
|
- Run `cargo run -p yaak-cli -- --help` to test the CLI
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
crates-tauri/yaak-app/vendored/**/* linguist-generated=true
|
crates-tauri/yaak-app-client/vendored/**/* linguist-generated=true
|
||||||
crates-tauri/yaak-app/gen/schemas/**/* linguist-generated=true
|
crates-tauri/yaak-app-client/gen/schemas/**/* linguist-generated=true
|
||||||
**/bindings/* linguist-generated=true
|
**/bindings/* linguist-generated=true
|
||||||
crates/yaak-templates/pkg/* linguist-generated=true
|
crates/yaak-templates/pkg/* linguist-generated=true
|
||||||
|
|
||||||
|
|||||||
@@ -125,8 +125,8 @@ jobs:
|
|||||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||||
|
|
||||||
# Sign vendored binaries with hardened runtime and their specific entitlements
|
# Sign vendored binaries with hardened runtime and their specific entitlements
|
||||||
codesign --force --options runtime --entitlements crates-tauri/yaak-app/macos/entitlements.yaakprotoc.plist --sign "$APPLE_SIGNING_IDENTITY" crates-tauri/yaak-app/vendored/protoc/yaakprotoc || true
|
codesign --force --options runtime --entitlements crates-tauri/yaak-app-client/macos/entitlements.yaakprotoc.plist --sign "$APPLE_SIGNING_IDENTITY" crates-tauri/yaak-app-client/vendored/protoc/yaakprotoc || true
|
||||||
codesign --force --options runtime --entitlements crates-tauri/yaak-app/macos/entitlements.yaaknode.plist --sign "$APPLE_SIGNING_IDENTITY" crates-tauri/yaak-app/vendored/node/yaaknode || true
|
codesign --force --options runtime --entitlements crates-tauri/yaak-app-client/macos/entitlements.yaaknode.plist --sign "$APPLE_SIGNING_IDENTITY" crates-tauri/yaak-app-client/vendored/node/yaaknode || true
|
||||||
|
|
||||||
- uses: tauri-apps/tauri-action@v0
|
- uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
@@ -155,7 +155,8 @@ jobs:
|
|||||||
releaseBody: "[Changelog __VERSION__](https://yaak.app/blog/__VERSION__)"
|
releaseBody: "[Changelog __VERSION__](https://yaak.app/blog/__VERSION__)"
|
||||||
releaseDraft: true
|
releaseDraft: true
|
||||||
prerelease: true
|
prerelease: true
|
||||||
args: "${{ matrix.args }} --config ./crates-tauri/yaak-app/tauri.release.conf.json"
|
projectPath: ./crates-tauri/yaak-app-client
|
||||||
|
args: "${{ matrix.args }} --config ./tauri.release.conf.json"
|
||||||
|
|
||||||
# Build a per-machine NSIS installer for enterprise deployment (PDQ, SCCM, Intune)
|
# Build a per-machine NSIS installer for enterprise deployment (PDQ, SCCM, Intune)
|
||||||
- name: Build and upload machine-wide installer (Windows only)
|
- name: Build and upload machine-wide installer (Windows only)
|
||||||
@@ -171,7 +172,9 @@ jobs:
|
|||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
Get-ChildItem -Recurse -Path target -File -Filter "*.exe.sig" | Remove-Item -Force
|
Get-ChildItem -Recurse -Path target -File -Filter "*.exe.sig" | Remove-Item -Force
|
||||||
npx tauri bundle ${{ matrix.args }} --bundles nsis --config ./crates-tauri/yaak-app/tauri.release.conf.json --config '{"bundle":{"createUpdaterArtifacts":true,"windows":{"nsis":{"installMode":"perMachine"}}}}'
|
Push-Location crates-tauri/yaak-app-client
|
||||||
|
npx tauri bundle ${{ matrix.args }} --bundles nsis --config ./tauri.release.conf.json --config '{"bundle":{"createUpdaterArtifacts":true,"windows":{"nsis":{"installMode":"perMachine"}}}}'
|
||||||
|
Pop-Location
|
||||||
$setup = Get-ChildItem -Recurse -Path target -Filter "*setup*.exe" | Select-Object -First 1
|
$setup = Get-ChildItem -Recurse -Path target -Filter "*setup*.exe" | Select-Object -First 1
|
||||||
$setupSig = "$($setup.FullName).sig"
|
$setupSig = "$($setup.FullName).sig"
|
||||||
$dest = $setup.FullName -replace '-setup\.exe$', '-setup-machine.exe'
|
$dest = $setup.FullName -replace '-setup\.exe$', '-setup-machine.exe'
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: vendored-assets
|
name: vendored-assets
|
||||||
path: |
|
path: |
|
||||||
crates-tauri/yaak-app/vendored/plugin-runtime/index.cjs
|
crates-tauri/yaak-app-client/vendored/plugin-runtime/index.cjs
|
||||||
crates-tauri/yaak-app/vendored/plugins
|
crates-tauri/yaak-app-client/vendored/plugins
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build-binaries:
|
build-binaries:
|
||||||
@@ -107,7 +107,7 @@ jobs:
|
|||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: vendored-assets
|
name: vendored-assets
|
||||||
path: crates-tauri/yaak-app/vendored
|
path: crates-tauri/yaak-app-client/vendored
|
||||||
|
|
||||||
- name: Set CLI build version
|
- name: Set CLI build version
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
+2
-1
@@ -39,7 +39,8 @@ codebook.toml
|
|||||||
target
|
target
|
||||||
|
|
||||||
# Per-worktree Tauri config (generated by post-checkout hook)
|
# Per-worktree Tauri config (generated by post-checkout hook)
|
||||||
crates-tauri/yaak-app/tauri.worktree.conf.json
|
crates-tauri/yaak-app-client/tauri.worktree.conf.json
|
||||||
|
crates-tauri/yaak-app-proxy/tauri.worktree.conf.json
|
||||||
|
|
||||||
# Tauri auto-generated permission files
|
# Tauri auto-generated permission files
|
||||||
**/permissions/autogenerated
|
**/permissions/autogenerated
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
**/bindings/**
|
||||||
|
**/routeTree.gen.ts
|
||||||
|
crates/yaak-templates/pkg/**
|
||||||
+5
-1
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"ignorePatterns": ["**/bindings/**", "crates/yaak-templates/pkg/**", "src-web/routeTree.gen.ts"]
|
"ignorePatterns": [
|
||||||
|
"**/bindings/**",
|
||||||
|
"crates/yaak-templates/pkg/**",
|
||||||
|
"apps/yaak-client/routeTree.gen.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
vp lint
|
vp lint
|
||||||
|
vp staged
|
||||||
|
|||||||
Generated
+546
-623
File diff suppressed because it is too large
Load Diff
+22
-5
@@ -2,6 +2,9 @@
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"crates/yaak",
|
"crates/yaak",
|
||||||
|
# Common/foundation crates
|
||||||
|
"crates/common/yaak-database",
|
||||||
|
"crates/common/yaak-rpc",
|
||||||
# Shared crates (no Tauri dependency)
|
# Shared crates (no Tauri dependency)
|
||||||
"crates/yaak-core",
|
"crates/yaak-core",
|
||||||
"crates/yaak-common",
|
"crates/yaak-common",
|
||||||
@@ -17,14 +20,19 @@ members = [
|
|||||||
"crates/yaak-tls",
|
"crates/yaak-tls",
|
||||||
"crates/yaak-ws",
|
"crates/yaak-ws",
|
||||||
"crates/yaak-api",
|
"crates/yaak-api",
|
||||||
|
"crates/yaak-proxy",
|
||||||
|
# Proxy-specific crates
|
||||||
|
"crates-proxy/yaak-proxy-lib",
|
||||||
# CLI crates
|
# CLI crates
|
||||||
"crates-cli/yaak-cli",
|
"crates-cli/yaak-cli",
|
||||||
# Tauri-specific crates
|
# Tauri-specific crates
|
||||||
"crates-tauri/yaak-app",
|
"crates-tauri/yaak-app-client",
|
||||||
|
"crates-tauri/yaak-app-proxy",
|
||||||
"crates-tauri/yaak-fonts",
|
"crates-tauri/yaak-fonts",
|
||||||
"crates-tauri/yaak-license",
|
"crates-tauri/yaak-license",
|
||||||
"crates-tauri/yaak-mac-window",
|
"crates-tauri/yaak-mac-window",
|
||||||
"crates-tauri/yaak-tauri-utils",
|
"crates-tauri/yaak-tauri-utils",
|
||||||
|
"crates-tauri/yaak-window",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
@@ -39,14 +47,18 @@ schemars = { version = "0.8.22", features = ["chrono"] }
|
|||||||
serde = "1.0.228"
|
serde = "1.0.228"
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
tauri = "2.9.5"
|
tauri = "2.11.1"
|
||||||
tauri-plugin = "2.5.2"
|
tauri-plugin = "2.6.1"
|
||||||
tauri-plugin-dialog = "2.4.2"
|
tauri-plugin-dialog = "2.7.1"
|
||||||
tauri-plugin-shell = "2.3.3"
|
tauri-plugin-shell = "2.3.5"
|
||||||
thiserror = "2.0.17"
|
thiserror = "2.0.17"
|
||||||
tokio = "1.48.0"
|
tokio = "1.48.0"
|
||||||
ts-rs = "11.1.0"
|
ts-rs = "11.1.0"
|
||||||
|
|
||||||
|
# Internal crates - common/foundation
|
||||||
|
yaak-database = { path = "crates/common/yaak-database" }
|
||||||
|
yaak-rpc = { path = "crates/common/yaak-rpc" }
|
||||||
|
|
||||||
# Internal crates - shared
|
# Internal crates - shared
|
||||||
yaak-core = { path = "crates/yaak-core" }
|
yaak-core = { path = "crates/yaak-core" }
|
||||||
yaak = { path = "crates/yaak" }
|
yaak = { path = "crates/yaak" }
|
||||||
@@ -63,12 +75,17 @@ yaak-templates = { path = "crates/yaak-templates" }
|
|||||||
yaak-tls = { path = "crates/yaak-tls" }
|
yaak-tls = { path = "crates/yaak-tls" }
|
||||||
yaak-ws = { path = "crates/yaak-ws" }
|
yaak-ws = { path = "crates/yaak-ws" }
|
||||||
yaak-api = { path = "crates/yaak-api" }
|
yaak-api = { path = "crates/yaak-api" }
|
||||||
|
yaak-proxy = { path = "crates/yaak-proxy" }
|
||||||
|
|
||||||
|
# Internal crates - proxy
|
||||||
|
yaak-proxy-lib = { path = "crates-proxy/yaak-proxy-lib" }
|
||||||
|
|
||||||
# Internal crates - Tauri-specific
|
# Internal crates - Tauri-specific
|
||||||
yaak-fonts = { path = "crates-tauri/yaak-fonts" }
|
yaak-fonts = { path = "crates-tauri/yaak-fonts" }
|
||||||
yaak-license = { path = "crates-tauri/yaak-license" }
|
yaak-license = { path = "crates-tauri/yaak-license" }
|
||||||
yaak-mac-window = { path = "crates-tauri/yaak-mac-window" }
|
yaak-mac-window = { path = "crates-tauri/yaak-mac-window" }
|
||||||
yaak-tauri-utils = { path = "crates-tauri/yaak-tauri-utils" }
|
yaak-tauri-utils = { path = "crates-tauri/yaak-tauri-utils" }
|
||||||
|
yaak-window = { path = "crates-tauri/yaak-window" }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
strip = false
|
strip = false
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/JamesIves/github-sponsors-readme-action">
|
<a href="https://github.com/JamesIves/github-sponsors-readme-action">
|
||||||
<img width="200px" src="https://github.com/mountain-loop/yaak/raw/main/crates-tauri/yaak-app/icons/icon.png">
|
<img width="200px" src="https://github.com/mountain-loop/yaak/raw/main/crates-tauri/yaak-app-client/icons/icon.png">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { createWorkspaceModel, type Folder, modelTypeLabel } from "@yaakapp-internal/models";
|
import { createWorkspaceModel, type Folder, modelTypeLabel } from "@yaakapp-internal/models";
|
||||||
import { applySync, calculateSync } from "@yaakapp-internal/sync";
|
import { applySync, calculateSync } from "@yaakapp-internal/sync";
|
||||||
import { Banner } from "../components/core/Banner";
|
|
||||||
import { Button } from "../components/core/Button";
|
import { Button } from "../components/core/Button";
|
||||||
import { InlineCode } from "../components/core/InlineCode";
|
|
||||||
import {
|
import {
|
||||||
|
Banner,
|
||||||
|
InlineCode,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
TableHeaderCell,
|
TableHeaderCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
TruncatedWideTableCell,
|
TruncatedWideTableCell,
|
||||||
} from "../components/core/Table";
|
} from "@yaakapp-internal/ui";
|
||||||
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
|
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
|
||||||
import { createFastMutation } from "../hooks/useFastMutation";
|
import { createFastMutation } from "../hooks/useFastMutation";
|
||||||
import { showDialog } from "../lib/dialog";
|
import { showDialog } from "../lib/dialog";
|
||||||
+1
-10
@@ -1,19 +1,10 @@
|
|||||||
import type { WorkspaceSettingsTab } from "../components/WorkspaceSettingsDialog";
|
import type { WorkspaceSettingsTab } from "../components/WorkspaceSettingsDialog";
|
||||||
import { WorkspaceSettingsDialog } from "../components/WorkspaceSettingsDialog";
|
import { WorkspaceSettingsDialog } from "../components/WorkspaceSettingsDialog";
|
||||||
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
|
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
|
||||||
import { showDialog } from "../lib/dialog";
|
|
||||||
import { jotaiStore } from "../lib/jotai";
|
import { jotaiStore } from "../lib/jotai";
|
||||||
|
|
||||||
export function openWorkspaceSettings(tab?: WorkspaceSettingsTab) {
|
export function openWorkspaceSettings(tab?: WorkspaceSettingsTab) {
|
||||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||||
if (workspaceId == null) return;
|
if (workspaceId == null) return;
|
||||||
showDialog({
|
WorkspaceSettingsDialog.show(workspaceId, tab);
|
||||||
id: "workspace-settings",
|
|
||||||
size: "md",
|
|
||||||
className: "h-[calc(100vh-5rem)] !max-h-[40rem]",
|
|
||||||
noPadding: true,
|
|
||||||
render: ({ hide }) => (
|
|
||||||
<WorkspaceSettingsDialog workspaceId={workspaceId} hide={hide} tab={tab} />
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
+1
-3
@@ -1,10 +1,8 @@
|
|||||||
import type { HttpRequest } from "@yaakapp-internal/models";
|
import type { HttpRequest } from "@yaakapp-internal/models";
|
||||||
|
import { Banner, HStack, InlineCode, VStack } from "@yaakapp-internal/ui";
|
||||||
import mime from "mime";
|
import mime from "mime";
|
||||||
import { useKeyValue } from "../hooks/useKeyValue";
|
import { useKeyValue } from "../hooks/useKeyValue";
|
||||||
import { Banner } from "./core/Banner";
|
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { InlineCode } from "./core/InlineCode";
|
|
||||||
import { HStack, VStack } from "./core/Stacks";
|
|
||||||
import { SelectFile } from "./SelectFile";
|
import { SelectFile } from "./SelectFile";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
+1
-2
@@ -1,15 +1,14 @@
|
|||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
import { gitClone } from "@yaakapp-internal/git";
|
import { gitClone } from "@yaakapp-internal/git";
|
||||||
|
import { Banner, VStack } from "@yaakapp-internal/ui";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir";
|
import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir";
|
||||||
import { appInfo } from "../lib/appInfo";
|
import { appInfo } from "../lib/appInfo";
|
||||||
import { showErrorToast } from "../lib/toast";
|
import { showErrorToast } from "../lib/toast";
|
||||||
import { Banner } from "./core/Banner";
|
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { Checkbox } from "./core/Checkbox";
|
import { Checkbox } from "./core/Checkbox";
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { PlainInput } from "./core/PlainInput";
|
import { PlainInput } from "./core/PlainInput";
|
||||||
import { VStack } from "./core/Stacks";
|
|
||||||
import { promptCredentials } from "./git/credentials";
|
import { promptCredentials } from "./git/credentials";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
+9
-10
@@ -1,4 +1,5 @@
|
|||||||
import { workspacesAtom } from "@yaakapp-internal/models";
|
import { workspacesAtom } from "@yaakapp-internal/models";
|
||||||
|
import { Heading, Icon, useDebouncedState } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { fuzzyFilter } from "fuzzbunny";
|
import { fuzzyFilter } from "fuzzbunny";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
import { createFolder } from "../commands/commands";
|
import { createFolder } from "../commands/commands";
|
||||||
import { createSubEnvironmentAndActivate } from "../commands/createEnvironment";
|
import { createSubEnvironmentAndActivate } from "../commands/createEnvironment";
|
||||||
import { openSettings } from "../commands/openSettings";
|
import { openSettings } from "../commands/openSettings";
|
||||||
|
import { openWorkspaceSettings } from "../commands/openWorkspaceSettings";
|
||||||
import { switchWorkspace } from "../commands/switchWorkspace";
|
import { switchWorkspace } from "../commands/switchWorkspace";
|
||||||
import { useActiveCookieJar } from "../hooks/useActiveCookieJar";
|
import { useActiveCookieJar } from "../hooks/useActiveCookieJar";
|
||||||
import { useActiveEnvironment } from "../hooks/useActiveEnvironment";
|
import { useActiveEnvironment } from "../hooks/useActiveEnvironment";
|
||||||
@@ -21,7 +23,6 @@ import { useActiveRequest } from "../hooks/useActiveRequest";
|
|||||||
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
|
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
|
||||||
import { useAllRequests } from "../hooks/useAllRequests";
|
import { useAllRequests } from "../hooks/useAllRequests";
|
||||||
import { useCreateWorkspace } from "../hooks/useCreateWorkspace";
|
import { useCreateWorkspace } from "../hooks/useCreateWorkspace";
|
||||||
import { useDebouncedState } from "../hooks/useDebouncedState";
|
|
||||||
import { useEnvironmentsBreakdown } from "../hooks/useEnvironmentsBreakdown";
|
import { useEnvironmentsBreakdown } from "../hooks/useEnvironmentsBreakdown";
|
||||||
import { useGrpcRequestActions } from "../hooks/useGrpcRequestActions";
|
import { useGrpcRequestActions } from "../hooks/useGrpcRequestActions";
|
||||||
import type { HotkeyAction } from "../hooks/useHotKey";
|
import type { HotkeyAction } from "../hooks/useHotKey";
|
||||||
@@ -36,7 +37,6 @@ import { appInfo } from "../lib/appInfo";
|
|||||||
import { copyToClipboard } from "../lib/copy";
|
import { copyToClipboard } from "../lib/copy";
|
||||||
import { createRequestAndNavigate } from "../lib/createRequestAndNavigate";
|
import { createRequestAndNavigate } from "../lib/createRequestAndNavigate";
|
||||||
import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
|
import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
|
||||||
import { showDialog } from "../lib/dialog";
|
|
||||||
import { editEnvironment } from "../lib/editEnvironment";
|
import { editEnvironment } from "../lib/editEnvironment";
|
||||||
import { renameModelWithPrompt } from "../lib/renameModelWithPrompt";
|
import { renameModelWithPrompt } from "../lib/renameModelWithPrompt";
|
||||||
import {
|
import {
|
||||||
@@ -47,10 +47,8 @@ import { router } from "../lib/router";
|
|||||||
import { setWorkspaceSearchParams } from "../lib/setWorkspaceSearchParams";
|
import { setWorkspaceSearchParams } from "../lib/setWorkspaceSearchParams";
|
||||||
import { CookieDialog } from "./CookieDialog";
|
import { CookieDialog } from "./CookieDialog";
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { Heading } from "./core/Heading";
|
|
||||||
import { Hotkey } from "./core/Hotkey";
|
import { Hotkey } from "./core/Hotkey";
|
||||||
import { HttpMethodTag } from "./core/HttpMethodTag";
|
import { HttpMethodTag } from "./core/HttpMethodTag";
|
||||||
import { Icon } from "./core/Icon";
|
|
||||||
import { PlainInput } from "./core/PlainInput";
|
import { PlainInput } from "./core/PlainInput";
|
||||||
|
|
||||||
interface CommandPaletteGroup {
|
interface CommandPaletteGroup {
|
||||||
@@ -101,6 +99,12 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
|||||||
action: "settings.show",
|
action: "settings.show",
|
||||||
onSelect: () => openSettings.mutate(null),
|
onSelect: () => openSettings.mutate(null),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "workspace_settings.open",
|
||||||
|
label: "Open Workspace Settings",
|
||||||
|
action: "workspace_settings.show",
|
||||||
|
onSelect: () => openWorkspaceSettings(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "app.create",
|
key: "app.create",
|
||||||
label: "Create Workspace",
|
label: "Create Workspace",
|
||||||
@@ -130,12 +134,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
|||||||
key: "cookies.show",
|
key: "cookies.show",
|
||||||
label: "Show Cookies",
|
label: "Show Cookies",
|
||||||
onSelect: async () => {
|
onSelect: async () => {
|
||||||
showDialog({
|
CookieDialog.show(activeCookieJar?.id ?? null);
|
||||||
id: "cookies",
|
|
||||||
title: "Manage Cookies",
|
|
||||||
size: "full",
|
|
||||||
render: () => <CookieDialog cookieJarId={activeCookieJar?.id ?? null} />,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
+1
-3
@@ -1,14 +1,12 @@
|
|||||||
import type { HttpRequest } from "@yaakapp-internal/models";
|
import type { HttpRequest } from "@yaakapp-internal/models";
|
||||||
import { patchModel } from "@yaakapp-internal/models";
|
import { patchModel } from "@yaakapp-internal/models";
|
||||||
|
import { Banner, HStack, InlineCode } from "@yaakapp-internal/ui";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useToggle } from "../hooks/useToggle";
|
import { useToggle } from "../hooks/useToggle";
|
||||||
import { showConfirm } from "../lib/confirm";
|
import { showConfirm } from "../lib/confirm";
|
||||||
import { Banner } from "./core/Banner";
|
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { InlineCode } from "./core/InlineCode";
|
|
||||||
import { Link } from "./core/Link";
|
import { Link } from "./core/Link";
|
||||||
import { SizeTag } from "./core/SizeTag";
|
import { SizeTag } from "./core/SizeTag";
|
||||||
import { HStack } from "./core/Stacks";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
+1
-3
@@ -1,4 +1,5 @@
|
|||||||
import type { HttpResponse } from "@yaakapp-internal/models";
|
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||||
|
import { Banner, HStack, InlineCode } from "@yaakapp-internal/ui";
|
||||||
import { type ReactNode, useMemo } from "react";
|
import { type ReactNode, useMemo } from "react";
|
||||||
import { useSaveResponse } from "../hooks/useSaveResponse";
|
import { useSaveResponse } from "../hooks/useSaveResponse";
|
||||||
import { useToggle } from "../hooks/useToggle";
|
import { useToggle } from "../hooks/useToggle";
|
||||||
@@ -6,11 +7,8 @@ import { isProbablyTextContentType } from "../lib/contentType";
|
|||||||
import { getContentTypeFromHeaders } from "../lib/model_util";
|
import { getContentTypeFromHeaders } from "../lib/model_util";
|
||||||
import { getResponseBodyText } from "../lib/responseBody";
|
import { getResponseBodyText } from "../lib/responseBody";
|
||||||
import { CopyButton } from "./CopyButton";
|
import { CopyButton } from "./CopyButton";
|
||||||
import { Banner } from "./core/Banner";
|
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { InlineCode } from "./core/InlineCode";
|
|
||||||
import { SizeTag } from "./core/SizeTag";
|
import { SizeTag } from "./core/SizeTag";
|
||||||
import { HStack } from "./core/Stacks";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
+1
-3
@@ -1,15 +1,13 @@
|
|||||||
import type { HttpResponse } from "@yaakapp-internal/models";
|
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||||
|
import { Banner, HStack, InlineCode } from "@yaakapp-internal/ui";
|
||||||
import { type ReactNode, useMemo } from "react";
|
import { type ReactNode, useMemo } from "react";
|
||||||
import { getRequestBodyText as getHttpResponseRequestBodyText } from "../hooks/useHttpRequestBody";
|
import { getRequestBodyText as getHttpResponseRequestBodyText } from "../hooks/useHttpRequestBody";
|
||||||
import { useToggle } from "../hooks/useToggle";
|
import { useToggle } from "../hooks/useToggle";
|
||||||
import { isProbablyTextContentType } from "../lib/contentType";
|
import { isProbablyTextContentType } from "../lib/contentType";
|
||||||
import { getContentTypeFromHeaders } from "../lib/model_util";
|
import { getContentTypeFromHeaders } from "../lib/model_util";
|
||||||
import { CopyButton } from "./CopyButton";
|
import { CopyButton } from "./CopyButton";
|
||||||
import { Banner } from "./core/Banner";
|
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { InlineCode } from "./core/InlineCode";
|
|
||||||
import { SizeTag } from "./core/SizeTag";
|
import { SizeTag } from "./core/SizeTag";
|
||||||
import { HStack } from "./core/Stacks";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -0,0 +1,659 @@
|
|||||||
|
import type { Cookie } from "@yaakapp-internal/models";
|
||||||
|
import { cookieJarsAtom, patchModel } from "@yaakapp-internal/models";
|
||||||
|
import { formatDate } from "date-fns/format";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import { type ComponentProps, useMemo, useState } from "react";
|
||||||
|
import { showDialog } from "../lib/dialog";
|
||||||
|
import { jotaiStore } from "../lib/jotai";
|
||||||
|
import { cookieDomain } from "../lib/model_util";
|
||||||
|
import {
|
||||||
|
Icon,
|
||||||
|
SplitLayout,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableRow,
|
||||||
|
TruncatedWideTableCell,
|
||||||
|
} from "@yaakapp-internal/ui";
|
||||||
|
import { IconButton } from "./core/IconButton";
|
||||||
|
import { Checkbox } from "./core/Checkbox";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { EventDetailHeader } from "./core/EventViewer";
|
||||||
|
import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
|
||||||
|
import { EmptyStateText } from "./EmptyStateText";
|
||||||
|
import { PlainInput } from "./core/PlainInput";
|
||||||
|
import { Select } from "./core/Select";
|
||||||
|
import { showAlert } from "../lib/alert";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
cookieJarId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CookieDialog = ({ cookieJarId }: Props) => {
|
||||||
|
const cookieJars = useAtomValue(cookieJarsAtom);
|
||||||
|
const cookieJar = cookieJars?.find((c) => c.id === cookieJarId);
|
||||||
|
const [filter, setFilter] = useState("");
|
||||||
|
const [filterUpdateKey, setFilterUpdateKey] = useState(0);
|
||||||
|
const [selectedCookieKey, setSelectedCookieKey] = useState<string | null>(null);
|
||||||
|
const [editingCookieKey, setEditingCookieKey] = useState<string | null>(null);
|
||||||
|
const [draftCookie, setDraftCookie] = useState<Cookie | null>(null);
|
||||||
|
const [draftExpiresInput, setDraftExpiresInput] = useState("");
|
||||||
|
const filteredCookies = useMemo(() => {
|
||||||
|
return cookieJar?.cookies.filter((cookie) => cookieMatchesFilter(cookie, filter)) ?? [];
|
||||||
|
}, [cookieJar?.cookies, filter]);
|
||||||
|
const selectedCookie = useMemo(
|
||||||
|
() =>
|
||||||
|
selectedCookieKey == null
|
||||||
|
? null
|
||||||
|
: (filteredCookies.find((cookie) => cookieKey(cookie) === selectedCookieKey) ?? null),
|
||||||
|
[filteredCookies, selectedCookieKey],
|
||||||
|
);
|
||||||
|
const detailCookie = draftCookie ?? selectedCookie;
|
||||||
|
const isCreatingCookie = editingCookieKey === NEW_COOKIE_KEY;
|
||||||
|
const isEditingCookie = draftCookie != null;
|
||||||
|
|
||||||
|
const handleAddCookie = () => {
|
||||||
|
setSelectedCookieKey(null);
|
||||||
|
setEditingCookieKey(NEW_COOKIE_KEY);
|
||||||
|
setDraftCookie(newCookieDraft());
|
||||||
|
setDraftExpiresInput("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditCookie = () => {
|
||||||
|
if (selectedCookie == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditingCookieKey(cookieKey(selectedCookie));
|
||||||
|
setDraftCookie(selectedCookie);
|
||||||
|
setDraftExpiresInput(cookieExpiresInputValue(selectedCookie));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelEdit = () => {
|
||||||
|
if (isCreatingCookie) {
|
||||||
|
setSelectedCookieKey(null);
|
||||||
|
}
|
||||||
|
setEditingCookieKey(null);
|
||||||
|
setDraftCookie(null);
|
||||||
|
setDraftExpiresInput("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseDetails = () => {
|
||||||
|
if (isEditingCookie) {
|
||||||
|
handleCancelEdit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedCookieKey(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveCookie = () => {
|
||||||
|
if (cookieJar == null || draftCookie == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextCookie = normalizeCookie(draftCookie);
|
||||||
|
if (nextCookie.name.trim().length === 0) {
|
||||||
|
showAlert({
|
||||||
|
id: "invalid-cookie-name",
|
||||||
|
title: "Invalid Cookie",
|
||||||
|
body: "Cookie name is required.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextCookie.expires !== "SessionEnd") {
|
||||||
|
const expires = cookieExpiresFromInput(draftExpiresInput);
|
||||||
|
if (expires == null) {
|
||||||
|
showAlert({
|
||||||
|
id: "invalid-cookie-expires",
|
||||||
|
title: "Invalid Cookie",
|
||||||
|
body: "Cookie expiration must be a valid date.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextCookie = { ...nextCookie, expires };
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextCookieKey = cookieKey(nextCookie);
|
||||||
|
const nextCookies = cookieJar.cookies.filter((cookie) => {
|
||||||
|
const key = cookieKey(cookie);
|
||||||
|
if (editingCookieKey != null && key === editingCookieKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return key !== nextCookieKey;
|
||||||
|
});
|
||||||
|
|
||||||
|
patchModel(cookieJar, { cookies: [...nextCookies, nextCookie] });
|
||||||
|
setSelectedCookieKey(nextCookieKey);
|
||||||
|
setEditingCookieKey(null);
|
||||||
|
setDraftCookie(null);
|
||||||
|
setDraftExpiresInput("");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cookieJar == null) {
|
||||||
|
return <div>No cookie jar selected</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pb-2 grid grid-rows-[auto_minmax(0,1fr)] space-y-2">
|
||||||
|
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-2">
|
||||||
|
<PlainInput
|
||||||
|
name="cookie-filter"
|
||||||
|
label="Filter cookies"
|
||||||
|
hideLabel
|
||||||
|
placeholder="Filter cookies"
|
||||||
|
defaultValue={filter}
|
||||||
|
forceUpdateKey={filterUpdateKey}
|
||||||
|
onChange={setFilter}
|
||||||
|
rightSlot={
|
||||||
|
filter.length > 0 && (
|
||||||
|
<IconButton
|
||||||
|
className="!bg-transparent !h-auto min-h-full opacity-50 hover:opacity-100 -mr-1"
|
||||||
|
icon="x"
|
||||||
|
title="Clear filter"
|
||||||
|
onClick={() => {
|
||||||
|
setFilter("");
|
||||||
|
setFilterUpdateKey((key) => key + 1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<IconButton icon="plus" size="sm" title="Add cookie" onClick={handleAddCookie} />
|
||||||
|
</div>
|
||||||
|
{cookieJar.cookies.length === 0 && detailCookie == null ? (
|
||||||
|
<EmptyStateText>
|
||||||
|
Cookies will appear when a response includes a Set-Cookie header.
|
||||||
|
</EmptyStateText>
|
||||||
|
) : filteredCookies.length === 0 && detailCookie == null ? (
|
||||||
|
<EmptyStateText>No cookies match the current filter.</EmptyStateText>
|
||||||
|
) : (
|
||||||
|
<SplitLayout
|
||||||
|
layout="vertical"
|
||||||
|
storageKey="cookie-dialog-details"
|
||||||
|
defaultRatio={0.65}
|
||||||
|
className="-mx-2"
|
||||||
|
minHeightPx={10}
|
||||||
|
firstSlot={({ style }) =>
|
||||||
|
filteredCookies.length === 0 ? (
|
||||||
|
<div style={style}>
|
||||||
|
<EmptyStateText>No cookies match the current filter.</EmptyStateText>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Table scrollable style={style} className="pr-0.5">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Value</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Domain</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Path</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Expires</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Size</TableHeaderCell>
|
||||||
|
<TableHeaderCell>HTTP Only</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Secure</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Same Site</TableHeaderCell>
|
||||||
|
<TableHeaderCell>
|
||||||
|
<IconButton
|
||||||
|
icon="list_x"
|
||||||
|
size="sm"
|
||||||
|
className="text-text-subtle"
|
||||||
|
title="Clear all cookies"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedCookieKey(null);
|
||||||
|
setEditingCookieKey(null);
|
||||||
|
setDraftCookie(null);
|
||||||
|
setDraftExpiresInput("");
|
||||||
|
patchModel(cookieJar, { cookies: [] });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody className="[&_td]:select-auto [&_td]:cursor-auto">
|
||||||
|
{filteredCookies.map((c: Cookie) => {
|
||||||
|
const key = cookieKey(c);
|
||||||
|
const isSelected = key === selectedCookieKey;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
key={key}
|
||||||
|
className={classNames(
|
||||||
|
"group/tr cursor-default",
|
||||||
|
isSelected && "[&_td]:bg-surface-highlight",
|
||||||
|
!isSelected && "hover:[&_td]:bg-surface-hover",
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedCookieKey(key);
|
||||||
|
setEditingCookieKey(null);
|
||||||
|
setDraftCookie(null);
|
||||||
|
setDraftExpiresInput("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableCell className={classNames("pl-2", isSelected && "rounded-l")}>
|
||||||
|
{c.name}
|
||||||
|
</TableCell>
|
||||||
|
<TruncatedWideTableCell className="min-w-[10rem]">
|
||||||
|
{c.value}
|
||||||
|
</TruncatedWideTableCell>
|
||||||
|
<TableCell>{cookieDomain(c)}</TableCell>
|
||||||
|
<TableCell>{c.path}</TableCell>
|
||||||
|
<TableCell>{cookieExpires(c)}</TableCell>
|
||||||
|
<TableCell>{cookieSize(c)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Icon
|
||||||
|
icon={c.httpOnly ? "check" : "x"}
|
||||||
|
className={classNames(!c.httpOnly && "opacity-10")}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Icon
|
||||||
|
icon={c.secure ? "check" : "x"}
|
||||||
|
className={classNames(!c.secure && "opacity-10")}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{c.sameSite}</TableCell>
|
||||||
|
<TableCell className="rounded-r pr-2">
|
||||||
|
<IconButton
|
||||||
|
icon="trash"
|
||||||
|
size="xs"
|
||||||
|
iconSize="sm"
|
||||||
|
title="Delete"
|
||||||
|
className="text-text-subtlest ml-auto group-hover/tr:text-text transition-colors"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (isSelected) {
|
||||||
|
setSelectedCookieKey(null);
|
||||||
|
}
|
||||||
|
if (editingCookieKey === key) {
|
||||||
|
setEditingCookieKey(null);
|
||||||
|
setDraftCookie(null);
|
||||||
|
setDraftExpiresInput("");
|
||||||
|
}
|
||||||
|
patchModel(cookieJar, {
|
||||||
|
cookies: cookieJar.cookies.filter(
|
||||||
|
(c2: Cookie) => cookieKey(c2) !== key,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
secondSlot={
|
||||||
|
detailCookie == null
|
||||||
|
? null
|
||||||
|
: ({ style }) => (
|
||||||
|
<div
|
||||||
|
style={style}
|
||||||
|
className="grid grid-rows-[auto_minmax(0,1fr)] bg-surface border-t border-border pt-2"
|
||||||
|
>
|
||||||
|
<EventDetailHeader
|
||||||
|
title={isCreatingCookie ? "New Cookie" : detailCookie.name || "Cookie"}
|
||||||
|
copyText={isEditingCookie ? undefined : detailCookie.value}
|
||||||
|
actions={
|
||||||
|
isEditingCookie
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "save",
|
||||||
|
label: isCreatingCookie ? "Create" : "Save",
|
||||||
|
onClick: handleSaveCookie,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "cancel",
|
||||||
|
label: "Cancel",
|
||||||
|
onClick: handleCancelEdit,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
key: "edit",
|
||||||
|
label: "Edit",
|
||||||
|
onClick: handleEditCookie,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
onClose={handleCloseDetails}
|
||||||
|
/>
|
||||||
|
{isEditingCookie ? (
|
||||||
|
<CookieEditor
|
||||||
|
cookie={detailCookie}
|
||||||
|
expiresInputValue={draftExpiresInput}
|
||||||
|
onChange={setDraftCookie}
|
||||||
|
onExpiresInputChange={setDraftExpiresInput}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CookieDetails cookie={detailCookie} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CookieDialog.show = (cookieJarId: string | null) => {
|
||||||
|
const cookieJar = jotaiStore.get(cookieJarsAtom)?.find((jar) => jar.id === cookieJarId);
|
||||||
|
if (cookieJar == null) {
|
||||||
|
showAlert({
|
||||||
|
id: "invalid-jar",
|
||||||
|
body: `Failed to find cookie jar for ID: ${cookieJarId}`,
|
||||||
|
title: "Invalid Cookie Jar",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showDialog({
|
||||||
|
id: "cookies",
|
||||||
|
title: `${cookieJar.name} Cookies`,
|
||||||
|
size: "full",
|
||||||
|
render: () => <CookieDialog cookieJarId={cookieJarId} />,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function CookieDetails({ cookie }: { cookie: Cookie }) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-y-auto">
|
||||||
|
<KeyValueRows selectable>
|
||||||
|
<CookieKeyValueRow label="Name">{cookie.name}</CookieKeyValueRow>
|
||||||
|
<CookieKeyValueRow label="Value" enableCopy copyText={cookie.value}>
|
||||||
|
<pre className="whitespace-pre-wrap break-all">{cookie.value}</pre>
|
||||||
|
</CookieKeyValueRow>
|
||||||
|
<CookieKeyValueRow label="Domain">{cookieDomain(cookie)}</CookieKeyValueRow>
|
||||||
|
<CookieKeyValueRow label="Path">{cookie.path}</CookieKeyValueRow>
|
||||||
|
<CookieKeyValueRow label="Expires">{cookieExpires(cookie)}</CookieKeyValueRow>
|
||||||
|
<CookieKeyValueRow label="Size">{cookieSize(cookie)}</CookieKeyValueRow>
|
||||||
|
<CookieKeyValueRow label="HTTP Only">{cookie.httpOnly ? "Yes" : "No"}</CookieKeyValueRow>
|
||||||
|
<CookieKeyValueRow label="Secure">{cookie.secure ? "Yes" : "No"}</CookieKeyValueRow>
|
||||||
|
{cookie.sameSite && (
|
||||||
|
<CookieKeyValueRow label="Same Site">{cookie.sameSite}</CookieKeyValueRow>
|
||||||
|
)}
|
||||||
|
</KeyValueRows>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CookieEditor({
|
||||||
|
cookie,
|
||||||
|
expiresInputValue,
|
||||||
|
onChange,
|
||||||
|
onExpiresInputChange,
|
||||||
|
}: {
|
||||||
|
cookie: Cookie;
|
||||||
|
expiresInputValue: string;
|
||||||
|
onChange: (cookie: Cookie) => void;
|
||||||
|
onExpiresInputChange: (value: string) => void;
|
||||||
|
}) {
|
||||||
|
const sessionCookie = cookie.expires === "SessionEnd";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-y-auto">
|
||||||
|
<KeyValueRows>
|
||||||
|
<CookieKeyValueRow align="middle" label="Name">
|
||||||
|
<CookieTextInput
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
value={cookie.name}
|
||||||
|
onChange={(name) => onChange({ ...cookie, name })}
|
||||||
|
/>
|
||||||
|
</CookieKeyValueRow>
|
||||||
|
<CookieKeyValueRow label="Value">
|
||||||
|
<CookieTextarea
|
||||||
|
value={cookie.value}
|
||||||
|
onChange={(value) => onChange({ ...cookie, value })}
|
||||||
|
/>
|
||||||
|
</CookieKeyValueRow>
|
||||||
|
<CookieKeyValueRow align="middle" label="Domain">
|
||||||
|
<CookieTextInput
|
||||||
|
value={cookieDomainInputValue(cookie)}
|
||||||
|
placeholder="n/a"
|
||||||
|
onChange={(domain) => onChange(cookieWithDomain(cookie, domain))}
|
||||||
|
/>
|
||||||
|
</CookieKeyValueRow>
|
||||||
|
<CookieKeyValueRow align="middle" label="Path">
|
||||||
|
<CookieTextInput
|
||||||
|
value={cookie.path}
|
||||||
|
placeholder="/"
|
||||||
|
onChange={(path) => onChange({ ...cookie, path })}
|
||||||
|
/>
|
||||||
|
</CookieKeyValueRow>
|
||||||
|
<CookieKeyValueRow label="Expires">
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Checkbox
|
||||||
|
checked={sessionCookie}
|
||||||
|
title="Session cookie"
|
||||||
|
onChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
onChange({ ...cookie, expires: "SessionEnd" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expiresInput =
|
||||||
|
cookieExpiresFromInput(expiresInputValue) == null
|
||||||
|
? defaultCookieExpiresInputValue()
|
||||||
|
: expiresInputValue;
|
||||||
|
|
||||||
|
onExpiresInputChange(expiresInput);
|
||||||
|
onChange({
|
||||||
|
...cookie,
|
||||||
|
expires: cookieExpiresFromInput(expiresInput)!,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CookieTextInput
|
||||||
|
value={sessionCookie ? "" : expiresInputValue}
|
||||||
|
disabled={sessionCookie}
|
||||||
|
onChange={(value) => {
|
||||||
|
onExpiresInputChange(value);
|
||||||
|
|
||||||
|
const expires = cookieExpiresFromInput(value);
|
||||||
|
if (expires != null) {
|
||||||
|
onChange({ ...cookie, expires });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CookieKeyValueRow>
|
||||||
|
<CookieKeyValueRow label="Size">{cookieSize(cookie)}</CookieKeyValueRow>
|
||||||
|
<CookieKeyValueRow align="middle" label="HTTP Only">
|
||||||
|
<Checkbox
|
||||||
|
hideLabel
|
||||||
|
title="HTTP Only"
|
||||||
|
checked={cookie.httpOnly}
|
||||||
|
onChange={(httpOnly) => onChange({ ...cookie, httpOnly })}
|
||||||
|
/>
|
||||||
|
</CookieKeyValueRow>
|
||||||
|
<CookieKeyValueRow align="middle" label="Secure">
|
||||||
|
<Checkbox
|
||||||
|
hideLabel
|
||||||
|
title="Secure"
|
||||||
|
checked={cookie.secure}
|
||||||
|
onChange={(secure) => onChange({ ...cookie, secure })}
|
||||||
|
/>
|
||||||
|
</CookieKeyValueRow>
|
||||||
|
<CookieKeyValueRow align="middle" label="Same Site">
|
||||||
|
<Select
|
||||||
|
hideLabel
|
||||||
|
name="cookie-same-site"
|
||||||
|
label="Same Site"
|
||||||
|
value={cookie.sameSite ?? ""}
|
||||||
|
size="xs"
|
||||||
|
className="w-full"
|
||||||
|
options={[
|
||||||
|
{ label: "n/a", value: "" },
|
||||||
|
{ label: "Lax", value: "Lax" },
|
||||||
|
{ label: "Strict", value: "Strict" },
|
||||||
|
{ label: "None", value: "None" },
|
||||||
|
]}
|
||||||
|
onChange={(sameSite) =>
|
||||||
|
onChange({
|
||||||
|
...cookie,
|
||||||
|
sameSite: sameSite === "" ? null : (sameSite as Cookie["sameSite"]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</CookieKeyValueRow>
|
||||||
|
</KeyValueRows>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CookieKeyValueRow({ labelClassName, ...props }: ComponentProps<typeof KeyValueRow>) {
|
||||||
|
return <KeyValueRow labelClassName={classNames("w-[7rem]", labelClassName)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CookieTextInput({
|
||||||
|
autoFocus,
|
||||||
|
disabled,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
required,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
autoFocus?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
required?: boolean;
|
||||||
|
value: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
className={cookieInputClassName}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
required={required}
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CookieTextarea({ onChange, value }: { onChange: (value: string) => void; value: string }) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={classNames(cookieInputClassName, "min-h-[5rem] resize-y")}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const NEW_COOKIE_KEY = "__new-cookie__";
|
||||||
|
const cookieInputClassName = classNames(
|
||||||
|
"w-full min-w-0 rounded bg-transparent px-1 py-0.5",
|
||||||
|
"border border-transparent outline-none",
|
||||||
|
"hover:border-border-subtle focus:border-border-focus",
|
||||||
|
"disabled:opacity-disabled disabled:border-dotted",
|
||||||
|
);
|
||||||
|
|
||||||
|
function cookieSize(cookie: Cookie) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
return encoder.encode(cookie.name).length + encoder.encode(cookie.value).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function newCookieDraft(): Cookie {
|
||||||
|
return {
|
||||||
|
name: "",
|
||||||
|
value: "",
|
||||||
|
domain: "NotPresent",
|
||||||
|
expires: "SessionEnd",
|
||||||
|
path: "/",
|
||||||
|
secure: false,
|
||||||
|
httpOnly: false,
|
||||||
|
sameSite: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCookie(cookie: Cookie): Cookie {
|
||||||
|
return {
|
||||||
|
...cookie,
|
||||||
|
name: cookie.name.trim(),
|
||||||
|
path: cookie.path.trim() || "/",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function cookieDomainInputValue(cookie: Cookie) {
|
||||||
|
const domain = cookieDomain(cookie);
|
||||||
|
return domain === "n/a" ? "" : domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cookieWithDomain(cookie: Cookie, domain: string): Cookie {
|
||||||
|
const trimmedDomain = domain.trim();
|
||||||
|
if (trimmedDomain.length === 0) {
|
||||||
|
return { ...cookie, domain: "NotPresent" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cookie.domain !== "NotPresent" && cookie.domain !== "Empty" && "Suffix" in cookie.domain) {
|
||||||
|
return { ...cookie, domain: { Suffix: trimmedDomain } };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...cookie, domain: { HostOnly: trimmedDomain } };
|
||||||
|
}
|
||||||
|
|
||||||
|
function cookieExpires(cookie: Cookie) {
|
||||||
|
if (cookie.expires === "SessionEnd") {
|
||||||
|
return "Session";
|
||||||
|
}
|
||||||
|
|
||||||
|
const expiresSeconds = Number(cookie.expires.AtUtc);
|
||||||
|
if (!Number.isFinite(expiresSeconds)) {
|
||||||
|
return cookie.expires.AtUtc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(expiresSeconds * 1000);
|
||||||
|
return formatDate(date, "MMM d, yyyy, h:mm:ss a");
|
||||||
|
}
|
||||||
|
|
||||||
|
function cookieExpiresInputValue(cookie: Cookie) {
|
||||||
|
if (cookie.expires === "SessionEnd") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const expiresSeconds = Number(cookie.expires.AtUtc);
|
||||||
|
if (!Number.isFinite(expiresSeconds)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(expiresSeconds * 1000).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultCookieExpiresInputValue() {
|
||||||
|
return new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cookieExpiresFromInput(value: string): Cookie["expires"] | null {
|
||||||
|
const time = new Date(value).getTime();
|
||||||
|
if (!Number.isFinite(time)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { AtUtc: `${Math.floor(time / 1000)}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
function cookieMatchesFilter(cookie: Cookie, filter: string) {
|
||||||
|
const query = filter.trim().toLowerCase();
|
||||||
|
if (query.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [cookie.name, cookie.value, cookieDomain(cookie)].some((value) =>
|
||||||
|
value.toLowerCase().includes(query),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cookieKey(cookie: Cookie) {
|
||||||
|
return JSON.stringify([cookie.name, cookieDomain(cookie), cookie.path]);
|
||||||
|
}
|
||||||
+2
-9
@@ -4,14 +4,12 @@ import { memo, useMemo } from "react";
|
|||||||
import { useActiveCookieJar } from "../hooks/useActiveCookieJar";
|
import { useActiveCookieJar } from "../hooks/useActiveCookieJar";
|
||||||
import { useCreateCookieJar } from "../hooks/useCreateCookieJar";
|
import { useCreateCookieJar } from "../hooks/useCreateCookieJar";
|
||||||
import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
|
import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
|
||||||
import { showDialog } from "../lib/dialog";
|
|
||||||
import { showPrompt } from "../lib/prompt";
|
import { showPrompt } from "../lib/prompt";
|
||||||
import { setWorkspaceSearchParams } from "../lib/setWorkspaceSearchParams";
|
import { setWorkspaceSearchParams } from "../lib/setWorkspaceSearchParams";
|
||||||
import { CookieDialog } from "./CookieDialog";
|
import { CookieDialog } from "./CookieDialog";
|
||||||
import { Dropdown, type DropdownItem } from "./core/Dropdown";
|
import { Dropdown, type DropdownItem } from "./core/Dropdown";
|
||||||
import { Icon } from "./core/Icon";
|
import { Icon, InlineCode } from "@yaakapp-internal/ui";
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { InlineCode } from "./core/InlineCode";
|
|
||||||
|
|
||||||
export const CookieDropdown = memo(function CookieDropdown() {
|
export const CookieDropdown = memo(function CookieDropdown() {
|
||||||
const activeCookieJar = useActiveCookieJar();
|
const activeCookieJar = useActiveCookieJar();
|
||||||
@@ -37,12 +35,7 @@ export const CookieDropdown = memo(function CookieDropdown() {
|
|||||||
leftSlot: <Icon icon="cookie" />,
|
leftSlot: <Icon icon="cookie" />,
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
if (activeCookieJar == null) return;
|
if (activeCookieJar == null) return;
|
||||||
showDialog({
|
CookieDialog.show(activeCookieJar.id);
|
||||||
id: "cookies",
|
|
||||||
title: "Manage Cookies",
|
|
||||||
size: "full",
|
|
||||||
render: () => <CookieDialog cookieJarId={activeCookieJar.id} />,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useTimedBoolean } from "../hooks/useTimedBoolean";
|
import { useTimedBoolean } from "@yaakapp-internal/ui";
|
||||||
import { copyToClipboard } from "../lib/copy";
|
import { copyToClipboard } from "../lib/copy";
|
||||||
import { showToast } from "../lib/toast";
|
import { showToast } from "../lib/toast";
|
||||||
import type { ButtonProps } from "./core/Button";
|
import type { ButtonProps } from "./core/Button";
|
||||||
+1
-3
@@ -1,8 +1,6 @@
|
|||||||
import { useTimedBoolean } from "../hooks/useTimedBoolean";
|
import { IconButton, type IconButtonProps, useTimedBoolean } from "@yaakapp-internal/ui";
|
||||||
import { copyToClipboard } from "../lib/copy";
|
import { copyToClipboard } from "../lib/copy";
|
||||||
import { showToast } from "../lib/toast";
|
import { showToast } from "../lib/toast";
|
||||||
import type { IconButtonProps } from "./core/IconButton";
|
|
||||||
import { IconButton } from "./core/IconButton";
|
|
||||||
|
|
||||||
interface Props extends Omit<IconButtonProps, "onClick" | "icon"> {
|
interface Props extends Omit<IconButtonProps, "onClick" | "icon"> {
|
||||||
text: string | (() => Promise<string | null>);
|
text: string | (() => Promise<string | null>);
|
||||||
+1
-1
@@ -1,6 +1,7 @@
|
|||||||
import { gitMutations } from "@yaakapp-internal/git";
|
import { gitMutations } from "@yaakapp-internal/git";
|
||||||
import type { WorkspaceMeta } from "@yaakapp-internal/models";
|
import type { WorkspaceMeta } from "@yaakapp-internal/models";
|
||||||
import { createGlobalModel, updateModel } from "@yaakapp-internal/models";
|
import { createGlobalModel, updateModel } from "@yaakapp-internal/models";
|
||||||
|
import { VStack } from "@yaakapp-internal/ui";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { router } from "../lib/router";
|
import { router } from "../lib/router";
|
||||||
import { setupOrConfigureEncryption } from "../lib/setupOrConfigureEncryption";
|
import { setupOrConfigureEncryption } from "../lib/setupOrConfigureEncryption";
|
||||||
@@ -10,7 +11,6 @@ import { Button } from "./core/Button";
|
|||||||
import { Checkbox } from "./core/Checkbox";
|
import { Checkbox } from "./core/Checkbox";
|
||||||
import { Label } from "./core/Label";
|
import { Label } from "./core/Label";
|
||||||
import { PlainInput } from "./core/PlainInput";
|
import { PlainInput } from "./core/PlainInput";
|
||||||
import { VStack } from "./core/Stacks";
|
|
||||||
import { EncryptionHelp } from "./EncryptionHelp";
|
import { EncryptionHelp } from "./EncryptionHelp";
|
||||||
import { gitCallbacks } from "./git/callbacks";
|
import { gitCallbacks } from "./git/callbacks";
|
||||||
import { SyncToFilesystemSetting } from "./SyncToFilesystemSetting";
|
import { SyncToFilesystemSetting } from "./SyncToFilesystemSetting";
|
||||||
+11
-3
@@ -1,13 +1,21 @@
|
|||||||
import type { DnsOverride, Workspace } from "@yaakapp-internal/models";
|
import type { DnsOverride, Workspace } from "@yaakapp-internal/models";
|
||||||
import { patchModel } from "@yaakapp-internal/models";
|
import { patchModel } from "@yaakapp-internal/models";
|
||||||
import { useCallback, useId, useMemo } from "react";
|
|
||||||
import { fireAndForget } from "../lib/fireAndForget";
|
import { fireAndForget } from "../lib/fireAndForget";
|
||||||
|
import {
|
||||||
|
HStack,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableRow,
|
||||||
|
VStack,
|
||||||
|
} from "@yaakapp-internal/ui";
|
||||||
|
import { useCallback, useId, useMemo } from "react";
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { Checkbox } from "./core/Checkbox";
|
import { Checkbox } from "./core/Checkbox";
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { PlainInput } from "./core/PlainInput";
|
import { PlainInput } from "./core/PlainInput";
|
||||||
import { HStack, VStack } from "./core/Stacks";
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "./core/Table";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
FormInputText,
|
FormInputText,
|
||||||
JsonPrimitive,
|
JsonPrimitive,
|
||||||
} from "@yaakapp-internal/plugins";
|
} from "@yaakapp-internal/plugins";
|
||||||
|
import { Banner, VStack } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useCallback, useEffect, useMemo } from "react";
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
@@ -19,7 +20,6 @@ import { useRandomKey } from "../hooks/useRandomKey";
|
|||||||
import { capitalize } from "../lib/capitalize";
|
import { capitalize } from "../lib/capitalize";
|
||||||
import { showDialog } from "../lib/dialog";
|
import { showDialog } from "../lib/dialog";
|
||||||
import { resolvedModelName } from "../lib/resolvedModelName";
|
import { resolvedModelName } from "../lib/resolvedModelName";
|
||||||
import { Banner } from "./core/Banner";
|
|
||||||
import { Checkbox } from "./core/Checkbox";
|
import { Checkbox } from "./core/Checkbox";
|
||||||
import { DetailsBanner } from "./core/DetailsBanner";
|
import { DetailsBanner } from "./core/DetailsBanner";
|
||||||
import { Editor } from "./core/Editor/LazyEditor";
|
import { Editor } from "./core/Editor/LazyEditor";
|
||||||
@@ -31,7 +31,6 @@ import type { Pair } from "./core/PairEditor";
|
|||||||
import { PairEditor } from "./core/PairEditor";
|
import { PairEditor } from "./core/PairEditor";
|
||||||
import { PlainInput } from "./core/PlainInput";
|
import { PlainInput } from "./core/PlainInput";
|
||||||
import { Select } from "./core/Select";
|
import { Select } from "./core/Select";
|
||||||
import { VStack } from "./core/Stacks";
|
|
||||||
import { Markdown } from "./Markdown";
|
import { Markdown } from "./Markdown";
|
||||||
import { SelectFile } from "./SelectFile";
|
import { SelectFile } from "./SelectFile";
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { VStack } from "./core/Stacks";
|
import { VStack } from "@yaakapp-internal/ui";
|
||||||
|
|
||||||
export function EncryptionHelp() {
|
export function EncryptionHelp() {
|
||||||
return (
|
return (
|
||||||
+1
-1
@@ -8,7 +8,7 @@ import type { ButtonProps } from "./core/Button";
|
|||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import type { DropdownItem } from "./core/Dropdown";
|
import type { DropdownItem } from "./core/Dropdown";
|
||||||
import { Dropdown } from "./core/Dropdown";
|
import { Dropdown } from "./core/Dropdown";
|
||||||
import { Icon } from "./core/Icon";
|
import { Icon } from "@yaakapp-internal/ui";
|
||||||
import { EnvironmentColorIndicator } from "./EnvironmentColorIndicator";
|
import { EnvironmentColorIndicator } from "./EnvironmentColorIndicator";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ColorIndicator } from "./ColorIndicator";
|
import { ColorIndicator } from "./ColorIndicator";
|
||||||
import { Banner } from "./core/Banner";
|
import { Banner } from "@yaakapp-internal/ui";
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { ColorPickerWithThemeColors } from "./core/ColorPicker";
|
import { ColorPickerWithThemeColors } from "./core/ColorPicker";
|
||||||
|
|
||||||
+77
-46
@@ -1,34 +1,38 @@
|
|||||||
import type { Environment, Workspace } from "@yaakapp-internal/models";
|
import type { Environment, Workspace } from "@yaakapp-internal/models";
|
||||||
import { duplicateModel, patchModel } from "@yaakapp-internal/models";
|
import { duplicateModel, patchModel } from "@yaakapp-internal/models";
|
||||||
|
import type { TreeHandle, TreeNode, TreeProps } from "@yaakapp-internal/ui";
|
||||||
|
import { Banner, Icon, InlineCode, SplitLayout, Tree } from "@yaakapp-internal/ui";
|
||||||
import { atom, useAtomValue } from "jotai";
|
import { atom, useAtomValue } from "jotai";
|
||||||
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
|
import { atomFamily } from "jotai-family";
|
||||||
|
import { useCallback, useLayoutEffect, useRef, useState } from "react";
|
||||||
import { createSubEnvironmentAndActivate } from "../commands/createEnvironment";
|
import { createSubEnvironmentAndActivate } from "../commands/createEnvironment";
|
||||||
import { activeWorkspaceAtom, activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
|
import { activeWorkspaceAtom, activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
|
||||||
import {
|
import {
|
||||||
environmentsBreakdownAtom,
|
environmentsBreakdownAtom,
|
||||||
useEnvironmentsBreakdown,
|
useEnvironmentsBreakdown,
|
||||||
} from "../hooks/useEnvironmentsBreakdown";
|
} from "../hooks/useEnvironmentsBreakdown";
|
||||||
|
import { useHotKey } from "../hooks/useHotKey";
|
||||||
|
import { atomWithKVStorage } from "../lib/atoms/atomWithKVStorage";
|
||||||
import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
|
import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
|
||||||
import { fireAndForget } from "../lib/fireAndForget";
|
import { fireAndForget } from "../lib/fireAndForget";
|
||||||
import { jotaiStore } from "../lib/jotai";
|
import { jotaiStore } from "../lib/jotai";
|
||||||
import { isBaseEnvironment, isSubEnvironment } from "../lib/model_util";
|
import { isBaseEnvironment, isSubEnvironment } from "../lib/model_util";
|
||||||
import { resolvedModelName } from "../lib/resolvedModelName";
|
import { resolvedModelName } from "../lib/resolvedModelName";
|
||||||
import { showColorPicker } from "../lib/showColorPicker";
|
import { showColorPicker } from "../lib/showColorPicker";
|
||||||
import { Banner } from "./core/Banner";
|
|
||||||
import type { ContextMenuProps, DropdownItem } from "./core/Dropdown";
|
import type { ContextMenuProps, DropdownItem } from "./core/Dropdown";
|
||||||
import { Icon } from "./core/Icon";
|
import { ContextMenu } from "./core/Dropdown";
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { IconTooltip } from "./core/IconTooltip";
|
import { IconTooltip } from "./core/IconTooltip";
|
||||||
import { InlineCode } from "./core/InlineCode";
|
|
||||||
import type { PairEditorHandle } from "./core/PairEditor";
|
import type { PairEditorHandle } from "./core/PairEditor";
|
||||||
import { SplitLayout } from "./core/SplitLayout";
|
|
||||||
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 { EnvironmentColorIndicator } from "./EnvironmentColorIndicator";
|
||||||
import { EnvironmentEditor } from "./EnvironmentEditor";
|
import { EnvironmentEditor } from "./EnvironmentEditor";
|
||||||
import { EnvironmentSharableTooltip } from "./EnvironmentSharableTooltip";
|
import { EnvironmentSharableTooltip } from "./EnvironmentSharableTooltip";
|
||||||
|
|
||||||
|
const collapsedFamily = atomFamily((treeId: string) => {
|
||||||
|
const key = ["env_collapsed", treeId ?? "n/a"];
|
||||||
|
return atomWithKVStorage<Record<string, boolean>>(key, {});
|
||||||
|
});
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
initialEnvironmentId: string | null;
|
initialEnvironmentId: string | null;
|
||||||
setRef?: (ref: PairEditorHandle | null) => void;
|
setRef?: (ref: PairEditorHandle | null) => void;
|
||||||
@@ -49,7 +53,7 @@ export function EnvironmentEditDialog({ initialEnvironmentId, setRef }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SplitLayout
|
<SplitLayout
|
||||||
name="env_editor"
|
storageKey="env_editor"
|
||||||
defaultRatio={0.75}
|
defaultRatio={0.75}
|
||||||
layout="horizontal"
|
layout="horizontal"
|
||||||
className="gap-0"
|
className="gap-0"
|
||||||
@@ -113,7 +117,7 @@ function EnvironmentEditDialogSidebar({
|
|||||||
const treeRef = useRef<TreeHandle>(null);
|
const treeRef = useRef<TreeHandle>(null);
|
||||||
const { baseEnvironment, baseEnvironments } = useEnvironmentsBreakdown();
|
const { baseEnvironment, baseEnvironments } = useEnvironmentsBreakdown();
|
||||||
|
|
||||||
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
// oxlint-disable-next-line react-hooks/exhaustive-deps -- none
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (selectedEnvironmentId == null) return;
|
if (selectedEnvironmentId == null) return;
|
||||||
treeRef.current?.selectItem(selectedEnvironmentId);
|
treeRef.current?.selectItem(selectedEnvironmentId);
|
||||||
@@ -130,44 +134,60 @@ function EnvironmentEditDialogSidebar({
|
|||||||
[baseEnvironment?.id, selectedEnvironmentId, setSelectedEnvironmentId],
|
[baseEnvironment?.id, selectedEnvironmentId, setSelectedEnvironmentId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const actions = useMemo(() => {
|
const treeHasFocus = useCallback(() => treeRef.current?.hasFocus() ?? false, []);
|
||||||
const enable = () => treeRef.current?.hasFocus() ?? false;
|
|
||||||
|
|
||||||
const actions = {
|
const getSelectedTreeModels = useCallback(
|
||||||
"sidebar.selected.rename": {
|
() => treeRef.current?.getSelectedItems() as TreeModel[] | undefined,
|
||||||
enable,
|
[],
|
||||||
allowDefault: true,
|
);
|
||||||
priority: 100,
|
|
||||||
cb: async (items: TreeModel[]) => {
|
const handleRenameSelected = useCallback(() => {
|
||||||
const item = items[0];
|
const items = getSelectedTreeModels();
|
||||||
if (items.length === 1 && item != null) {
|
if (items?.length === 1 && items[0] != null) {
|
||||||
treeRef.current?.renameItem(item.id);
|
treeRef.current?.renameItem(items[0].id);
|
||||||
}
|
}
|
||||||
},
|
}, [getSelectedTreeModels]);
|
||||||
},
|
|
||||||
"sidebar.selected.delete": {
|
const handleDeleteSelected = useCallback(
|
||||||
priority: 100,
|
(items: TreeModel[]) => deleteModelWithConfirm(items),
|
||||||
enable,
|
[],
|
||||||
cb: (items: TreeModel[]) => deleteModelWithConfirm(items),
|
);
|
||||||
},
|
|
||||||
"sidebar.selected.duplicate": {
|
const handleDuplicateSelected = useCallback(
|
||||||
priority: 100,
|
async (items: TreeModel[]) => {
|
||||||
enable,
|
|
||||||
cb: async (items: TreeModel[]) => {
|
|
||||||
if (items.length === 1 && items[0]) {
|
if (items.length === 1 && items[0]) {
|
||||||
const item = items[0];
|
const newId = await duplicateModel(items[0]);
|
||||||
const newId = await duplicateModel(item);
|
|
||||||
setSelectedEnvironmentId(newId);
|
setSelectedEnvironmentId(newId);
|
||||||
} else {
|
} else {
|
||||||
await Promise.all(items.map(duplicateModel));
|
await Promise.all(items.map(duplicateModel));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
[setSelectedEnvironmentId],
|
||||||
} as const;
|
);
|
||||||
return actions;
|
|
||||||
}, [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) {
|
||||||
|
fireAndForget(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(
|
const getContextMenu = useCallback(
|
||||||
(items: TreeModel[]): ContextMenuProps["items"] => {
|
(items: TreeModel[]): ContextMenuProps["items"] => {
|
||||||
@@ -196,12 +216,10 @@ function EnvironmentEditDialogSidebar({
|
|||||||
hidden: isBaseEnvironment(environment) || !singleEnvironment,
|
hidden: isBaseEnvironment(environment) || !singleEnvironment,
|
||||||
hotKeyAction: "sidebar.selected.rename",
|
hotKeyAction: "sidebar.selected.rename",
|
||||||
hotKeyLabelOnly: true,
|
hotKeyLabelOnly: true,
|
||||||
onSelect: async () => {
|
onSelect: () => {
|
||||||
// Not sure why this is needed, but without it the
|
// Not sure why this is needed, but without it the
|
||||||
// edit input blurs immediately after opening.
|
// edit input blurs immediately after opening.
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => handleRenameSelected());
|
||||||
fireAndForget(actions["sidebar.selected.rename"].cb(items));
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -210,7 +228,7 @@ function EnvironmentEditDialogSidebar({
|
|||||||
hidden: isBaseEnvironment(environment),
|
hidden: isBaseEnvironment(environment),
|
||||||
hotKeyAction: "sidebar.selected.duplicate",
|
hotKeyAction: "sidebar.selected.duplicate",
|
||||||
hotKeyLabelOnly: true,
|
hotKeyLabelOnly: true,
|
||||||
onSelect: () => actions["sidebar.selected.duplicate"].cb(items),
|
onSelect: () => handleDuplicateSelected(items),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: environment.color ? "Change Color" : "Assign Color",
|
label: environment.color ? "Change Color" : "Assign Color",
|
||||||
@@ -246,7 +264,12 @@ function EnvironmentEditDialogSidebar({
|
|||||||
|
|
||||||
return menuItems;
|
return menuItems;
|
||||||
},
|
},
|
||||||
[actions, baseEnvironments.length, handleDeleteEnvironment],
|
[
|
||||||
|
baseEnvironments.length,
|
||||||
|
handleDeleteEnvironment,
|
||||||
|
handleDuplicateSelected,
|
||||||
|
handleRenameSelected,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDragEnd = useCallback(async function handleDragEnd({
|
const handleDragEnd = useCallback(async function handleDragEnd({
|
||||||
@@ -293,6 +316,13 @@ function EnvironmentEditDialogSidebar({
|
|||||||
[setSelectedEnvironmentId],
|
[setSelectedEnvironmentId],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderContextMenuFn = useCallback<NonNullable<TreeProps<TreeModel>["renderContextMenu"]>>(
|
||||||
|
({ items, position, onClose }) => (
|
||||||
|
<ContextMenu items={items as DropdownItem[]} triggerPosition={position} onClose={onClose} />
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const tree = useAtomValue(treeAtom);
|
const tree = useAtomValue(treeAtom);
|
||||||
return (
|
return (
|
||||||
<aside className="x-theme-sidebar h-full w-full min-w-0 grid overflow-y-auto border-r border-border-subtle ">
|
<aside className="x-theme-sidebar h-full w-full min-w-0 grid overflow-y-auto border-r border-border-subtle ">
|
||||||
@@ -301,10 +331,11 @@ function EnvironmentEditDialogSidebar({
|
|||||||
<Tree
|
<Tree
|
||||||
ref={treeRef}
|
ref={treeRef}
|
||||||
treeId={treeId}
|
treeId={treeId}
|
||||||
|
collapsedAtom={collapsedFamily(treeId)}
|
||||||
className="px-2 pb-10"
|
className="px-2 pb-10"
|
||||||
hotkeys={hotkeys}
|
|
||||||
root={tree}
|
root={tree}
|
||||||
getContextMenu={getContextMenu}
|
getContextMenu={getContextMenu}
|
||||||
|
renderContextMenu={renderContextMenuFn}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
getItemKey={(i) => `${i.id}::${i.name}`}
|
getItemKey={(i) => `${i.id}::${i.name}`}
|
||||||
ItemLeftSlotInner={ItemLeftSlotInner}
|
ItemLeftSlotInner={ItemLeftSlotInner}
|
||||||
+1
-1
@@ -1,6 +1,7 @@
|
|||||||
import type { Environment } from "@yaakapp-internal/models";
|
import type { Environment } from "@yaakapp-internal/models";
|
||||||
import { patchModel } from "@yaakapp-internal/models";
|
import { patchModel } from "@yaakapp-internal/models";
|
||||||
import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
|
import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
|
||||||
|
import { Heading } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useEnvironmentsBreakdown } from "../hooks/useEnvironmentsBreakdown";
|
import { useEnvironmentsBreakdown } from "../hooks/useEnvironmentsBreakdown";
|
||||||
@@ -15,7 +16,6 @@ import {
|
|||||||
} from "../lib/setupOrConfigureEncryption";
|
} from "../lib/setupOrConfigureEncryption";
|
||||||
import { DismissibleBanner } from "./core/DismissibleBanner";
|
import { DismissibleBanner } from "./core/DismissibleBanner";
|
||||||
import type { GenericCompletionConfig } from "./core/Editor/genericCompletion";
|
import type { GenericCompletionConfig } from "./core/Editor/genericCompletion";
|
||||||
import { Heading } from "./core/Heading";
|
|
||||||
import type { PairEditorHandle, PairWithId } from "./core/PairEditor";
|
import type { PairEditorHandle, PairWithId } from "./core/PairEditor";
|
||||||
import { ensurePairId } from "./core/PairEditor.util";
|
import { ensurePairId } from "./core/PairEditor.util";
|
||||||
import { PairOrBulkEditor } from "./core/PairOrBulkEditor";
|
import { PairOrBulkEditor } from "./core/PairOrBulkEditor";
|
||||||
+1
-3
@@ -1,9 +1,7 @@
|
|||||||
|
import { Banner, Button, InlineCode } from "@yaakapp-internal/ui";
|
||||||
import type { ErrorInfo, ReactNode } from "react";
|
import type { ErrorInfo, ReactNode } from "react";
|
||||||
import { Component, useEffect } from "react";
|
import { Component, useEffect } from "react";
|
||||||
import { showDialog } from "../lib/dialog";
|
import { showDialog } from "../lib/dialog";
|
||||||
import { Banner } from "./core/Banner";
|
|
||||||
import { Button } from "./core/Button";
|
|
||||||
import { InlineCode } from "./core/InlineCode";
|
|
||||||
import RouteError from "./RouteError";
|
import RouteError from "./RouteError";
|
||||||
|
|
||||||
interface ErrorBoundaryProps {
|
interface ErrorBoundaryProps {
|
||||||
+1
-1
@@ -1,6 +1,7 @@
|
|||||||
import { save } from "@tauri-apps/plugin-dialog";
|
import { save } from "@tauri-apps/plugin-dialog";
|
||||||
import type { Workspace } from "@yaakapp-internal/models";
|
import type { Workspace } from "@yaakapp-internal/models";
|
||||||
import { workspacesAtom } from "@yaakapp-internal/models";
|
import { workspacesAtom } from "@yaakapp-internal/models";
|
||||||
|
import { HStack, VStack } from "@yaakapp-internal/ui";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import slugify from "slugify";
|
import slugify from "slugify";
|
||||||
@@ -11,7 +12,6 @@ import { Button } from "./core/Button";
|
|||||||
import { Checkbox } from "./core/Checkbox";
|
import { Checkbox } from "./core/Checkbox";
|
||||||
import { DetailsBanner } from "./core/DetailsBanner";
|
import { DetailsBanner } from "./core/DetailsBanner";
|
||||||
import { Link } from "./core/Link";
|
import { Link } from "./core/Link";
|
||||||
import { HStack, VStack } from "./core/Stacks";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onHide: () => void;
|
onHide: () => void;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Folder, GrpcRequest, HttpRequest, WebsocketRequest } from "@yaakapp-internal/models";
|
import type { Folder, GrpcRequest, HttpRequest, WebsocketRequest } from "@yaakapp-internal/models";
|
||||||
import { foldersAtom } from "@yaakapp-internal/models";
|
import { foldersAtom } from "@yaakapp-internal/models";
|
||||||
|
import { Heading, HStack, Icon, LoadingIcon } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import type { CSSProperties, ReactNode } from "react";
|
import type { CSSProperties, ReactNode } from "react";
|
||||||
@@ -8,20 +9,15 @@ import { allRequestsAtom } from "../hooks/useAllRequests";
|
|||||||
import { useFolderActions } from "../hooks/useFolderActions";
|
import { useFolderActions } from "../hooks/useFolderActions";
|
||||||
import { useLatestHttpResponse } from "../hooks/useLatestHttpResponse";
|
import { useLatestHttpResponse } from "../hooks/useLatestHttpResponse";
|
||||||
import { sendAnyHttpRequest } from "../hooks/useSendAnyHttpRequest";
|
import { sendAnyHttpRequest } from "../hooks/useSendAnyHttpRequest";
|
||||||
import { fireAndForget } from "../lib/fireAndForget";
|
|
||||||
import { showDialog } from "../lib/dialog";
|
import { showDialog } from "../lib/dialog";
|
||||||
import { resolvedModelName } from "../lib/resolvedModelName";
|
import { resolvedModelName } from "../lib/resolvedModelName";
|
||||||
import { router } from "../lib/router";
|
import { router } from "../lib/router";
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { Heading } from "./core/Heading";
|
|
||||||
import { HttpResponseDurationTag } from "./core/HttpResponseDurationTag";
|
import { HttpResponseDurationTag } from "./core/HttpResponseDurationTag";
|
||||||
import { HttpStatusTag } from "./core/HttpStatusTag";
|
import { HttpStatusTag } from "./core/HttpStatusTag";
|
||||||
import { Icon } from "./core/Icon";
|
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { LoadingIcon } from "./core/LoadingIcon";
|
|
||||||
import { Separator } from "./core/Separator";
|
import { Separator } from "./core/Separator";
|
||||||
import { SizeTag } from "./core/SizeTag";
|
import { SizeTag } from "./core/SizeTag";
|
||||||
import { HStack } from "./core/Stacks";
|
|
||||||
import { HttpResponsePane } from "./HttpResponsePane";
|
import { HttpResponsePane } from "./HttpResponsePane";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -46,7 +42,7 @@ export function FolderLayout({ folder, style }: Props) {
|
|||||||
}, [folder.id, folders, requests]);
|
}, [folder.id, folders, requests]);
|
||||||
|
|
||||||
const handleSendAll = useCallback(() => {
|
const handleSendAll = useCallback(() => {
|
||||||
if (sendAllAction) fireAndForget(sendAllAction.call(folder));
|
void sendAllAction?.call(folder);
|
||||||
}, [sendAllAction, folder]);
|
}, [sendAllAction, folder]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
+14
-4
@@ -1,4 +1,5 @@
|
|||||||
import { createWorkspaceModel, foldersAtom, patchModel } from "@yaakapp-internal/models";
|
import { createWorkspaceModel, foldersAtom, patchModel } from "@yaakapp-internal/models";
|
||||||
|
import { HStack, Icon, InlineCode, VStack } from "@yaakapp-internal/ui";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { Fragment, useMemo } from "react";
|
import { Fragment, useMemo } from "react";
|
||||||
import { useAuthTab } from "../hooks/useAuthTab";
|
import { useAuthTab } from "../hooks/useAuthTab";
|
||||||
@@ -11,11 +12,8 @@ import { hideDialog } from "../lib/dialog";
|
|||||||
import { CopyIconButton } from "./CopyIconButton";
|
import { CopyIconButton } from "./CopyIconButton";
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { CountBadge } from "./core/CountBadge";
|
import { CountBadge } from "./core/CountBadge";
|
||||||
import { Icon } from "./core/Icon";
|
|
||||||
import { InlineCode } from "./core/InlineCode";
|
|
||||||
import { Input } from "./core/Input";
|
import { Input } from "./core/Input";
|
||||||
import { Link } from "./core/Link";
|
import { Link } from "./core/Link";
|
||||||
import { HStack, VStack } from "./core/Stacks";
|
|
||||||
import type { TabItem } from "./core/Tabs/Tabs";
|
import type { TabItem } from "./core/Tabs/Tabs";
|
||||||
import { TabContent, Tabs } from "./core/Tabs/Tabs";
|
import { TabContent, Tabs } from "./core/Tabs/Tabs";
|
||||||
import { EmptyStateText } from "./EmptyStateText";
|
import { EmptyStateText } from "./EmptyStateText";
|
||||||
@@ -23,6 +21,7 @@ import { EnvironmentEditor } from "./EnvironmentEditor";
|
|||||||
import { HeadersEditor } from "./HeadersEditor";
|
import { HeadersEditor } from "./HeadersEditor";
|
||||||
import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
|
import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
|
||||||
import { MarkdownEditor } from "./MarkdownEditor";
|
import { MarkdownEditor } from "./MarkdownEditor";
|
||||||
|
import { countOverriddenSettings, ModelSettingsEditor } from "./ModelSettingsEditor";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
folderId: string | null;
|
folderId: string | null;
|
||||||
@@ -31,6 +30,7 @@ interface Props {
|
|||||||
|
|
||||||
const TAB_AUTH = "auth";
|
const TAB_AUTH = "auth";
|
||||||
const TAB_HEADERS = "headers";
|
const TAB_HEADERS = "headers";
|
||||||
|
const TAB_SETTINGS = "settings";
|
||||||
const TAB_VARIABLES = "variables";
|
const TAB_VARIABLES = "variables";
|
||||||
const TAB_GENERAL = "general";
|
const TAB_GENERAL = "general";
|
||||||
|
|
||||||
@@ -38,6 +38,7 @@ export type FolderSettingsTab =
|
|||||||
| typeof TAB_AUTH
|
| typeof TAB_AUTH
|
||||||
| typeof TAB_HEADERS
|
| typeof TAB_HEADERS
|
||||||
| typeof TAB_GENERAL
|
| typeof TAB_GENERAL
|
||||||
|
| typeof TAB_SETTINGS
|
||||||
| typeof TAB_VARIABLES;
|
| typeof TAB_VARIABLES;
|
||||||
|
|
||||||
export function FolderSettingsDialog({ folderId, tab }: Props) {
|
export function FolderSettingsDialog({ folderId, tab }: Props) {
|
||||||
@@ -53,6 +54,7 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
|||||||
(e) => e.parentModel === "folder" && e.parentId === folderId,
|
(e) => e.parentModel === "folder" && e.parentId === folderId,
|
||||||
);
|
);
|
||||||
const numVars = (folderEnvironment?.variables ?? []).filter((v) => v.name).length;
|
const numVars = (folderEnvironment?.variables ?? []).filter((v) => v.name).length;
|
||||||
|
const numSettingsOverrides = folder == null ? 0 : countOverriddenSettings(folder);
|
||||||
|
|
||||||
const tabs = useMemo<TabItem[]>(() => {
|
const tabs = useMemo<TabItem[]>(() => {
|
||||||
if (folder == null) return [];
|
if (folder == null) return [];
|
||||||
@@ -62,6 +64,11 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
|||||||
value: TAB_GENERAL,
|
value: TAB_GENERAL,
|
||||||
label: "General",
|
label: "General",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: TAB_SETTINGS,
|
||||||
|
label: "Settings",
|
||||||
|
rightSlot: <CountBadge count={numSettingsOverrides} />,
|
||||||
|
},
|
||||||
...headersTab,
|
...headersTab,
|
||||||
...authTab,
|
...authTab,
|
||||||
{
|
{
|
||||||
@@ -70,7 +77,7 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
|||||||
rightSlot: numVars > 0 ? <CountBadge count={numVars} /> : null,
|
rightSlot: numVars > 0 ? <CountBadge count={numVars} /> : null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [authTab, folder, headersTab, numVars]);
|
}, [authTab, folder, headersTab, numSettingsOverrides, numVars]);
|
||||||
|
|
||||||
if (folder == null) return null;
|
if (folder == null) return null;
|
||||||
|
|
||||||
@@ -161,6 +168,9 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
|||||||
stateKey={`headers.${folder.id}`}
|
stateKey={`headers.${folder.id}`}
|
||||||
/>
|
/>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
<TabContent value={TAB_SETTINGS} className="overflow-y-auto h-full px-4">
|
||||||
|
<ModelSettingsEditor model={folder} />
|
||||||
|
</TabContent>
|
||||||
<TabContent value={TAB_VARIABLES} className="overflow-y-auto h-full px-4">
|
<TabContent value={TAB_VARIABLES} className="overflow-y-auto h-full px-4">
|
||||||
{folderEnvironment == null ? (
|
{folderEnvironment == null ? (
|
||||||
<EmptyStateText>
|
<EmptyStateText>
|
||||||
+5
-3
@@ -7,10 +7,10 @@ import { useActiveRequest } from "../hooks/useActiveRequest";
|
|||||||
import { useGrpc } from "../hooks/useGrpc";
|
import { useGrpc } from "../hooks/useGrpc";
|
||||||
import { useGrpcProtoFiles } from "../hooks/useGrpcProtoFiles";
|
import { useGrpcProtoFiles } from "../hooks/useGrpcProtoFiles";
|
||||||
import { activeGrpcConnectionAtom, useGrpcEvents } from "../hooks/usePinnedGrpcConnection";
|
import { activeGrpcConnectionAtom, useGrpcEvents } from "../hooks/usePinnedGrpcConnection";
|
||||||
|
import { Banner, SplitLayout } from "@yaakapp-internal/ui";
|
||||||
|
import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
|
||||||
import { workspaceLayoutAtom } from "../lib/atoms";
|
import { workspaceLayoutAtom } from "../lib/atoms";
|
||||||
import { Banner } from "./core/Banner";
|
|
||||||
import { HotkeyList } from "./core/HotkeyList";
|
import { HotkeyList } from "./core/HotkeyList";
|
||||||
import { SplitLayout } from "./core/SplitLayout";
|
|
||||||
import { GrpcRequestPane } from "./GrpcRequestPane";
|
import { GrpcRequestPane } from "./GrpcRequestPane";
|
||||||
import { GrpcResponsePane } from "./GrpcResponsePane";
|
import { GrpcResponsePane } from "./GrpcResponsePane";
|
||||||
|
|
||||||
@@ -22,6 +22,8 @@ const emptyArray: string[] = [];
|
|||||||
|
|
||||||
export function GrpcConnectionLayout({ style }: Props) {
|
export function GrpcConnectionLayout({ style }: Props) {
|
||||||
const workspaceLayout = useAtomValue(workspaceLayoutAtom);
|
const workspaceLayout = useAtomValue(workspaceLayoutAtom);
|
||||||
|
const activeWorkspace = useAtomValue(activeWorkspaceAtom);
|
||||||
|
const wsId = activeWorkspace?.id ?? "n/a";
|
||||||
const activeRequest = useActiveRequest("grpc_request");
|
const activeRequest = useActiveRequest("grpc_request");
|
||||||
const activeConnection = useAtomValue(activeGrpcConnectionAtom);
|
const activeConnection = useAtomValue(activeGrpcConnectionAtom);
|
||||||
const grpcEvents = useGrpcEvents(activeConnection?.id ?? null);
|
const grpcEvents = useGrpcEvents(activeConnection?.id ?? null);
|
||||||
@@ -79,7 +81,7 @@ export function GrpcConnectionLayout({ style }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SplitLayout
|
<SplitLayout
|
||||||
name="grpc_layout"
|
storageKey={`grpc_layout::${wsId}`}
|
||||||
className="p-3 gap-1.5"
|
className="p-3 gap-1.5"
|
||||||
style={style}
|
style={style}
|
||||||
layout={workspaceLayout}
|
layout={workspaceLayout}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { jsoncLanguage } from "@shopify/lang-jsonc";
|
|
||||||
import { linter } from "@codemirror/lint";
|
import { linter } from "@codemirror/lint";
|
||||||
import type { EditorView } from "@codemirror/view";
|
import type { EditorView } from "@codemirror/view";
|
||||||
|
import { jsoncLanguage } from "@shopify/lang-jsonc";
|
||||||
import type { GrpcRequest } from "@yaakapp-internal/models";
|
import type { GrpcRequest } from "@yaakapp-internal/models";
|
||||||
|
import { FormattedError, InlineCode, VStack } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {
|
import {
|
||||||
handleRefresh,
|
handleRefresh,
|
||||||
@@ -18,9 +19,6 @@ import { pluralizeCount } from "../lib/pluralize";
|
|||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import type { EditorProps } from "./core/Editor/Editor";
|
import type { EditorProps } from "./core/Editor/Editor";
|
||||||
import { Editor } from "./core/Editor/LazyEditor";
|
import { Editor } from "./core/Editor/LazyEditor";
|
||||||
import { FormattedError } from "./core/FormattedError";
|
|
||||||
import { InlineCode } from "./core/InlineCode";
|
|
||||||
import { VStack } from "./core/Stacks";
|
|
||||||
import { GrpcProtoSelectionDialog } from "./GrpcProtoSelectionDialog";
|
import { GrpcProtoSelectionDialog } from "./GrpcProtoSelectionDialog";
|
||||||
|
|
||||||
type Props = Pick<EditorProps, "heightMode" | "onChange" | "className" | "forceUpdateKey"> & {
|
type Props = Pick<EditorProps, "heightMode" | "onChange" | "className" | "forceUpdateKey"> & {
|
||||||
+3
-6
@@ -1,16 +1,13 @@
|
|||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
import type { GrpcRequest } from "@yaakapp-internal/models";
|
import type { GrpcRequest } from "@yaakapp-internal/models";
|
||||||
|
import { Banner, HStack, Icon, InlineCode, VStack } from "@yaakapp-internal/ui";
|
||||||
import { useActiveRequest } from "../hooks/useActiveRequest";
|
import { useActiveRequest } from "../hooks/useActiveRequest";
|
||||||
import { useGrpc } from "../hooks/useGrpc";
|
import { useGrpc } from "../hooks/useGrpc";
|
||||||
import { useGrpcProtoFiles } from "../hooks/useGrpcProtoFiles";
|
import { useGrpcProtoFiles } from "../hooks/useGrpcProtoFiles";
|
||||||
import { pluralizeCount } from "../lib/pluralize";
|
import { pluralizeCount } from "../lib/pluralize";
|
||||||
import { Banner } from "./core/Banner";
|
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { Icon } from "./core/Icon";
|
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { InlineCode } from "./core/InlineCode";
|
|
||||||
import { Link } from "./core/Link";
|
import { Link } from "./core/Link";
|
||||||
import { HStack, VStack } from "./core/Stacks";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onDone: () => void;
|
onDone: () => void;
|
||||||
@@ -30,7 +27,7 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
|
|||||||
const services = grpc.reflect.data;
|
const services = grpc.reflect.data;
|
||||||
const serverReflection = protoFiles.length === 0 && services != null;
|
const serverReflection = protoFiles.length === 0 && services != null;
|
||||||
let reflectError = grpc.reflect.error ?? null;
|
let reflectError = grpc.reflect.error ?? null;
|
||||||
const reflectionUnimplemented = `${reflectError}`.match(/unimplemented/i);
|
const reflectionUnimplemented = String(reflectError).match(/unimplemented/i);
|
||||||
|
|
||||||
if (reflectionUnimplemented) {
|
if (reflectionUnimplemented) {
|
||||||
reflectError = null;
|
reflectError = null;
|
||||||
@@ -143,8 +140,8 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
|
|||||||
<tbody className="divide-y divide-surface-highlight">
|
<tbody className="divide-y divide-surface-highlight">
|
||||||
{protoFiles.map((f, i) => {
|
{protoFiles.map((f, i) => {
|
||||||
const parts = f.split("/");
|
const parts = f.split("/");
|
||||||
|
// oxlint-disable-next-line no-array-index-key -- none
|
||||||
return (
|
return (
|
||||||
// oxlint-disable-next-line react/no-array-index-key
|
|
||||||
<tr key={f + i} className="group">
|
<tr key={f + i} className="group">
|
||||||
<td>
|
<td>
|
||||||
<Icon icon={f.endsWith(".proto") ? "file_code" : "folder_code"} />
|
<Icon icon={f.endsWith(".proto") ? "file_code" : "folder_code"} />
|
||||||
+13
-4
@@ -1,9 +1,9 @@
|
|||||||
import { type GrpcRequest, type HttpRequestHeader, patchModel } from "@yaakapp-internal/models";
|
import { type GrpcRequest, type HttpRequestHeader, patchModel } from "@yaakapp-internal/models";
|
||||||
|
import { HStack, Icon, useContainerSize, VStack } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
import { useCallback, useMemo, useRef } from "react";
|
import { useCallback, useMemo, useRef } from "react";
|
||||||
import { useAuthTab } from "../hooks/useAuthTab";
|
import { useAuthTab } from "../hooks/useAuthTab";
|
||||||
import { useContainerSize } from "../hooks/useContainerQuery";
|
|
||||||
import type { ReflectResponseService } from "../hooks/useGrpc";
|
import type { ReflectResponseService } from "../hooks/useGrpc";
|
||||||
import { useHeadersTab } from "../hooks/useHeadersTab";
|
import { useHeadersTab } from "../hooks/useHeadersTab";
|
||||||
import { useInheritedHeaders } from "../hooks/useInheritedHeaders";
|
import { useInheritedHeaders } from "../hooks/useInheritedHeaders";
|
||||||
@@ -11,17 +11,16 @@ import { useRequestUpdateKey } from "../hooks/useRequestUpdateKey";
|
|||||||
import { resolvedModelName } from "../lib/resolvedModelName";
|
import { resolvedModelName } from "../lib/resolvedModelName";
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { CountBadge } from "./core/CountBadge";
|
import { CountBadge } from "./core/CountBadge";
|
||||||
import { Icon } from "./core/Icon";
|
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { PlainInput } from "./core/PlainInput";
|
import { PlainInput } from "./core/PlainInput";
|
||||||
import { RadioDropdown } from "./core/RadioDropdown";
|
import { RadioDropdown } from "./core/RadioDropdown";
|
||||||
import { HStack, VStack } from "./core/Stacks";
|
|
||||||
import type { TabItem } from "./core/Tabs/Tabs";
|
import type { TabItem } from "./core/Tabs/Tabs";
|
||||||
import { TabContent, Tabs } from "./core/Tabs/Tabs";
|
import { TabContent, Tabs } from "./core/Tabs/Tabs";
|
||||||
import { GrpcEditor } from "./GrpcEditor";
|
import { GrpcEditor } from "./GrpcEditor";
|
||||||
import { HeadersEditor } from "./HeadersEditor";
|
import { HeadersEditor } from "./HeadersEditor";
|
||||||
import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
|
import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
|
||||||
import { MarkdownEditor } from "./MarkdownEditor";
|
import { MarkdownEditor } from "./MarkdownEditor";
|
||||||
|
import { countOverriddenSettings, ModelSettingsEditor } from "./ModelSettingsEditor";
|
||||||
import { UrlBar } from "./UrlBar";
|
import { UrlBar } from "./UrlBar";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -49,6 +48,7 @@ interface Props {
|
|||||||
const TAB_MESSAGE = "message";
|
const TAB_MESSAGE = "message";
|
||||||
const TAB_METADATA = "metadata";
|
const TAB_METADATA = "metadata";
|
||||||
const TAB_AUTH = "auth";
|
const TAB_AUTH = "auth";
|
||||||
|
const TAB_SETTINGS = "settings";
|
||||||
const TAB_DESCRIPTION = "description";
|
const TAB_DESCRIPTION = "description";
|
||||||
|
|
||||||
export function GrpcRequestPane({
|
export function GrpcRequestPane({
|
||||||
@@ -68,6 +68,7 @@ export function GrpcRequestPane({
|
|||||||
const authTab = useAuthTab(TAB_AUTH, activeRequest);
|
const authTab = useAuthTab(TAB_AUTH, activeRequest);
|
||||||
const metadataTab = useHeadersTab(TAB_METADATA, activeRequest, "Metadata");
|
const metadataTab = useHeadersTab(TAB_METADATA, activeRequest, "Metadata");
|
||||||
const inheritedHeaders = useInheritedHeaders(activeRequest);
|
const inheritedHeaders = useInheritedHeaders(activeRequest);
|
||||||
|
const numSettingsOverrides = countOverriddenSettings(activeRequest);
|
||||||
const forceUpdateKey = useRequestUpdateKey(activeRequest.id ?? null);
|
const forceUpdateKey = useRequestUpdateKey(activeRequest.id ?? null);
|
||||||
|
|
||||||
const urlContainerEl = useRef<HTMLDivElement>(null);
|
const urlContainerEl = useRef<HTMLDivElement>(null);
|
||||||
@@ -130,13 +131,18 @@ export function GrpcRequestPane({
|
|||||||
{ value: TAB_MESSAGE, label: "Message" },
|
{ value: TAB_MESSAGE, label: "Message" },
|
||||||
...metadataTab,
|
...metadataTab,
|
||||||
...authTab,
|
...authTab,
|
||||||
|
{
|
||||||
|
value: TAB_SETTINGS,
|
||||||
|
label: "Settings",
|
||||||
|
rightSlot: <CountBadge count={numSettingsOverrides} />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: TAB_DESCRIPTION,
|
value: TAB_DESCRIPTION,
|
||||||
label: "Info",
|
label: "Info",
|
||||||
rightSlot: activeRequest.description && <CountBadge count={true} />,
|
rightSlot: activeRequest.description && <CountBadge count={true} />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[activeRequest.description, authTab, metadataTab],
|
[activeRequest.description, authTab, metadataTab, numSettingsOverrides],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleMetadataChange = useCallback(
|
const handleMetadataChange = useCallback(
|
||||||
@@ -280,6 +286,9 @@ export function GrpcRequestPane({
|
|||||||
onChange={handleMetadataChange}
|
onChange={handleMetadataChange}
|
||||||
/>
|
/>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
<TabContent value={TAB_SETTINGS}>
|
||||||
|
<ModelSettingsEditor model={activeRequest} />
|
||||||
|
</TabContent>
|
||||||
<TabContent value={TAB_DESCRIPTION}>
|
<TabContent value={TAB_DESCRIPTION}>
|
||||||
<div className="grid grid-rows-[auto_minmax(0,1fr)] h-full">
|
<div className="grid grid-rows-[auto_minmax(0,1fr)] h-full">
|
||||||
<PlainInput
|
<PlainInput
|
||||||
+2
-4
@@ -1,4 +1,5 @@
|
|||||||
import type { GrpcEvent, GrpcRequest } from "@yaakapp-internal/models";
|
import type { GrpcEvent, GrpcRequest } from "@yaakapp-internal/models";
|
||||||
|
import { HStack, Icon, type IconProps, LoadingIcon, VStack } from "@yaakapp-internal/ui";
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
@@ -14,10 +15,7 @@ import { Editor } from "./core/Editor/LazyEditor";
|
|||||||
import { EventDetailHeader, EventViewer } from "./core/EventViewer";
|
import { EventDetailHeader, EventViewer } from "./core/EventViewer";
|
||||||
import { EventViewerRow } from "./core/EventViewerRow";
|
import { EventViewerRow } from "./core/EventViewerRow";
|
||||||
import { HotkeyList } from "./core/HotkeyList";
|
import { HotkeyList } from "./core/HotkeyList";
|
||||||
import { Icon, type IconProps } from "./core/Icon";
|
|
||||||
import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
|
import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
|
||||||
import { LoadingIcon } from "./core/LoadingIcon";
|
|
||||||
import { HStack, VStack } from "./core/Stacks";
|
|
||||||
import { EmptyStateText } from "./EmptyStateText";
|
import { EmptyStateText } from "./EmptyStateText";
|
||||||
import { ErrorBoundary } from "./ErrorBoundary";
|
import { ErrorBoundary } from "./ErrorBoundary";
|
||||||
import { RecentGrpcConnectionsDropdown } from "./RecentGrpcConnectionsDropdown";
|
import { RecentGrpcConnectionsDropdown } from "./RecentGrpcConnectionsDropdown";
|
||||||
@@ -93,7 +91,7 @@ export function GrpcResponsePane({ style, methodType, activeRequest }: Props) {
|
|||||||
getEventKey={(event) => event.id}
|
getEventKey={(event) => event.id}
|
||||||
error={activeConnection.error}
|
error={activeConnection.error}
|
||||||
header={header}
|
header={header}
|
||||||
splitLayoutName="grpc_events"
|
splitLayoutStorageKey="grpc_events"
|
||||||
defaultRatio={0.4}
|
defaultRatio={0.4}
|
||||||
renderRow={({ event, isActive, onClick }) => (
|
renderRow={({ event, isActive, onClick }) => (
|
||||||
<GrpcEventRow event={event} isActive={isActive} onClick={onClick} />
|
<GrpcEventRow event={event} isActive={isActive} onClick={onClick} />
|
||||||
+1
-1
@@ -1,5 +1,6 @@
|
|||||||
import type { HttpRequestHeader } from "@yaakapp-internal/models";
|
import type { HttpRequestHeader } from "@yaakapp-internal/models";
|
||||||
import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
|
import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
|
||||||
|
import { HStack } from "@yaakapp-internal/ui";
|
||||||
import { charsets } from "../lib/data/charsets";
|
import { charsets } from "../lib/data/charsets";
|
||||||
import { connections } from "../lib/data/connections";
|
import { connections } from "../lib/data/connections";
|
||||||
import { encodings } from "../lib/data/encodings";
|
import { encodings } from "../lib/data/encodings";
|
||||||
@@ -13,7 +14,6 @@ import type { Pair, PairEditorProps } from "./core/PairEditor";
|
|||||||
import { PairEditorRow } from "./core/PairEditor";
|
import { PairEditorRow } from "./core/PairEditor";
|
||||||
import { ensurePairId } from "./core/PairEditor.util";
|
import { ensurePairId } from "./core/PairEditor.util";
|
||||||
import { PairOrBulkEditor } from "./core/PairOrBulkEditor";
|
import { PairOrBulkEditor } from "./core/PairOrBulkEditor";
|
||||||
import { HStack } from "./core/Stacks";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
forceUpdateKey: string;
|
forceUpdateKey: string;
|
||||||
+1
-3
@@ -6,6 +6,7 @@ import type {
|
|||||||
Workspace,
|
Workspace,
|
||||||
} from "@yaakapp-internal/models";
|
} from "@yaakapp-internal/models";
|
||||||
import { patchModel } from "@yaakapp-internal/models";
|
import { patchModel } from "@yaakapp-internal/models";
|
||||||
|
import { HStack, Icon, InlineCode } from "@yaakapp-internal/ui";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { openFolderSettings } from "../commands/openFolderSettings";
|
import { openFolderSettings } from "../commands/openFolderSettings";
|
||||||
import { openWorkspaceSettings } from "../commands/openWorkspaceSettings";
|
import { openWorkspaceSettings } from "../commands/openWorkspaceSettings";
|
||||||
@@ -14,13 +15,10 @@ import { useInheritedAuthentication } from "../hooks/useInheritedAuthentication"
|
|||||||
import { useRenderTemplate } from "../hooks/useRenderTemplate";
|
import { useRenderTemplate } from "../hooks/useRenderTemplate";
|
||||||
import { resolvedModelName } from "../lib/resolvedModelName";
|
import { resolvedModelName } from "../lib/resolvedModelName";
|
||||||
import { Dropdown, type DropdownItem } from "./core/Dropdown";
|
import { Dropdown, type DropdownItem } from "./core/Dropdown";
|
||||||
import { Icon } from "./core/Icon";
|
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { InlineCode } from "./core/InlineCode";
|
|
||||||
import { Input, type InputProps } from "./core/Input";
|
import { Input, type InputProps } from "./core/Input";
|
||||||
import { Link } from "./core/Link";
|
import { Link } from "./core/Link";
|
||||||
import { SegmentedControl } from "./core/SegmentedControl";
|
import { SegmentedControl } from "./core/SegmentedControl";
|
||||||
import { HStack } from "./core/Stacks";
|
|
||||||
import { DynamicForm } from "./DynamicForm";
|
import { DynamicForm } from "./DynamicForm";
|
||||||
import { EmptyStateText } from "./EmptyStateText";
|
import { EmptyStateText } from "./EmptyStateText";
|
||||||
|
|
||||||
+7
-4
@@ -1,11 +1,12 @@
|
|||||||
import type { HttpRequest } from "@yaakapp-internal/models";
|
import type { HttpRequest } from "@yaakapp-internal/models";
|
||||||
|
import type { SlotProps } from "@yaakapp-internal/ui";
|
||||||
|
import { SplitLayout } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
import { useCurrentGraphQLSchema } from "../hooks/useIntrospectGraphQL";
|
import { useCurrentGraphQLSchema } from "../hooks/useIntrospectGraphQL";
|
||||||
|
import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
|
||||||
import { workspaceLayoutAtom } from "../lib/atoms";
|
import { workspaceLayoutAtom } from "../lib/atoms";
|
||||||
import type { SlotProps } from "./core/SplitLayout";
|
|
||||||
import { SplitLayout } from "./core/SplitLayout";
|
|
||||||
import { GraphQLDocsExplorer } from "./graphql/GraphQLDocsExplorer";
|
import { GraphQLDocsExplorer } from "./graphql/GraphQLDocsExplorer";
|
||||||
import { showGraphQLDocExplorerAtom } from "./graphql/graphqlAtoms";
|
import { showGraphQLDocExplorerAtom } from "./graphql/graphqlAtoms";
|
||||||
import { HttpRequestPane } from "./HttpRequestPane";
|
import { HttpRequestPane } from "./HttpRequestPane";
|
||||||
@@ -20,10 +21,12 @@ export function HttpRequestLayout({ activeRequest, style }: Props) {
|
|||||||
const showGraphQLDocExplorer = useAtomValue(showGraphQLDocExplorerAtom);
|
const showGraphQLDocExplorer = useAtomValue(showGraphQLDocExplorerAtom);
|
||||||
const graphQLSchema = useCurrentGraphQLSchema(activeRequest);
|
const graphQLSchema = useCurrentGraphQLSchema(activeRequest);
|
||||||
const workspaceLayout = useAtomValue(workspaceLayoutAtom);
|
const workspaceLayout = useAtomValue(workspaceLayoutAtom);
|
||||||
|
const activeWorkspace = useAtomValue(activeWorkspaceAtom);
|
||||||
|
const wsId = activeWorkspace?.id ?? "n/a";
|
||||||
|
|
||||||
const requestResponseSplit = ({ style }: Pick<SlotProps, "style">) => (
|
const requestResponseSplit = ({ style }: Pick<SlotProps, "style">) => (
|
||||||
<SplitLayout
|
<SplitLayout
|
||||||
name="http_layout"
|
storageKey={`http_layout::${wsId}`}
|
||||||
className="p-3 gap-1.5"
|
className="p-3 gap-1.5"
|
||||||
style={style}
|
style={style}
|
||||||
layout={workspaceLayout}
|
layout={workspaceLayout}
|
||||||
@@ -47,7 +50,7 @@ export function HttpRequestLayout({ activeRequest, style }: Props) {
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<SplitLayout
|
<SplitLayout
|
||||||
name="graphql_layout"
|
storageKey={`graphql_layout::${wsId}`}
|
||||||
defaultRatio={1 / 3}
|
defaultRatio={1 / 3}
|
||||||
firstSlot={requestResponseSplit}
|
firstSlot={requestResponseSplit}
|
||||||
secondSlot={({ style, orientation }) => (
|
secondSlot={({ style, orientation }) => (
|
||||||
+13
-1
@@ -38,7 +38,7 @@ import { ConfirmLargeRequestBody } from "./ConfirmLargeRequestBody";
|
|||||||
import { CountBadge } from "./core/CountBadge";
|
import { CountBadge } from "./core/CountBadge";
|
||||||
import type { GenericCompletionConfig } from "./core/Editor/genericCompletion";
|
import type { GenericCompletionConfig } from "./core/Editor/genericCompletion";
|
||||||
import { Editor } from "./core/Editor/LazyEditor";
|
import { Editor } from "./core/Editor/LazyEditor";
|
||||||
import { InlineCode } from "./core/InlineCode";
|
import { InlineCode } from "@yaakapp-internal/ui";
|
||||||
import type { Pair } from "./core/PairEditor";
|
import type { Pair } from "./core/PairEditor";
|
||||||
import { PlainInput } from "./core/PlainInput";
|
import { PlainInput } from "./core/PlainInput";
|
||||||
import type { TabItem, TabsRef } from "./core/Tabs/Tabs";
|
import type { TabItem, TabsRef } from "./core/Tabs/Tabs";
|
||||||
@@ -51,6 +51,7 @@ import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
|
|||||||
import { JsonBodyEditor } from "./JsonBodyEditor";
|
import { JsonBodyEditor } from "./JsonBodyEditor";
|
||||||
import { MarkdownEditor } from "./MarkdownEditor";
|
import { MarkdownEditor } from "./MarkdownEditor";
|
||||||
import { RequestMethodDropdown } from "./RequestMethodDropdown";
|
import { RequestMethodDropdown } from "./RequestMethodDropdown";
|
||||||
|
import { countOverriddenSettings, ModelSettingsEditor } from "./ModelSettingsEditor";
|
||||||
import { UrlBar } from "./UrlBar";
|
import { UrlBar } from "./UrlBar";
|
||||||
import { UrlParametersEditor } from "./UrlParameterEditor";
|
import { UrlParametersEditor } from "./UrlParameterEditor";
|
||||||
|
|
||||||
@@ -69,6 +70,7 @@ const TAB_BODY = "body";
|
|||||||
const TAB_PARAMS = "params";
|
const TAB_PARAMS = "params";
|
||||||
const TAB_HEADERS = "headers";
|
const TAB_HEADERS = "headers";
|
||||||
const TAB_AUTH = "auth";
|
const TAB_AUTH = "auth";
|
||||||
|
const TAB_SETTINGS = "settings";
|
||||||
const TAB_DESCRIPTION = "description";
|
const TAB_DESCRIPTION = "description";
|
||||||
const TABS_STORAGE_KEY = "http_request_tabs";
|
const TABS_STORAGE_KEY = "http_request_tabs";
|
||||||
|
|
||||||
@@ -92,6 +94,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
const authTab = useAuthTab(TAB_AUTH, activeRequest);
|
const authTab = useAuthTab(TAB_AUTH, activeRequest);
|
||||||
const headersTab = useHeadersTab(TAB_HEADERS, activeRequest);
|
const headersTab = useHeadersTab(TAB_HEADERS, activeRequest);
|
||||||
const inheritedHeaders = useInheritedHeaders(activeRequest);
|
const inheritedHeaders = useInheritedHeaders(activeRequest);
|
||||||
|
const numSettingsOverrides = countOverriddenSettings(activeRequest);
|
||||||
|
|
||||||
// Listen for event to focus the params tab (e.g., when clicking a :param in the URL)
|
// Listen for event to focus the params tab (e.g., when clicking a :param in the URL)
|
||||||
useRequestEditorEvent(
|
useRequestEditorEvent(
|
||||||
@@ -234,6 +237,11 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
},
|
},
|
||||||
...headersTab,
|
...headersTab,
|
||||||
...authTab,
|
...authTab,
|
||||||
|
{
|
||||||
|
value: TAB_SETTINGS,
|
||||||
|
label: "Settings",
|
||||||
|
rightSlot: <CountBadge count={numSettingsOverrides} />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: TAB_DESCRIPTION,
|
value: TAB_DESCRIPTION,
|
||||||
label: "Info",
|
label: "Info",
|
||||||
@@ -246,6 +254,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
handleContentTypeChange,
|
handleContentTypeChange,
|
||||||
headersTab,
|
headersTab,
|
||||||
numParams,
|
numParams,
|
||||||
|
numSettingsOverrides,
|
||||||
urlParameterPairs.length,
|
urlParameterPairs.length,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -372,6 +381,9 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
onChange={(urlParameters) => patchModel(activeRequest, { urlParameters })}
|
onChange={(urlParameters) => patchModel(activeRequest, { urlParameters })}
|
||||||
/>
|
/>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
<TabContent value={TAB_SETTINGS}>
|
||||||
|
<ModelSettingsEditor model={activeRequest} />
|
||||||
|
</TabContent>
|
||||||
<TabContent value={TAB_BODY}>
|
<TabContent value={TAB_BODY}>
|
||||||
<ConfirmLargeRequestBody request={activeRequest}>
|
<ConfirmLargeRequestBody request={activeRequest}>
|
||||||
{activeRequest.bodyType === BODY_TYPE_JSON ? (
|
{activeRequest.bodyType === BODY_TYPE_JSON ? (
|
||||||
+1
-4
@@ -1,4 +1,5 @@
|
|||||||
import type { HttpResponse, HttpResponseEvent } from "@yaakapp-internal/models";
|
import type { HttpResponse, HttpResponseEvent } from "@yaakapp-internal/models";
|
||||||
|
import { Banner, HStack, Icon, LoadingIcon, VStack } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import type { ComponentType, CSSProperties } from "react";
|
import type { ComponentType, CSSProperties } from "react";
|
||||||
import { lazy, Suspense, useMemo } from "react";
|
import { lazy, Suspense, useMemo } from "react";
|
||||||
@@ -12,17 +13,13 @@ import { getMimeTypeFromContentType } from "../lib/contentType";
|
|||||||
import { getContentTypeFromHeaders, getCookieCounts } from "../lib/model_util";
|
import { getContentTypeFromHeaders, getCookieCounts } from "../lib/model_util";
|
||||||
import { ConfirmLargeResponse } from "./ConfirmLargeResponse";
|
import { ConfirmLargeResponse } from "./ConfirmLargeResponse";
|
||||||
import { ConfirmLargeResponseRequest } from "./ConfirmLargeResponseRequest";
|
import { ConfirmLargeResponseRequest } from "./ConfirmLargeResponseRequest";
|
||||||
import { Banner } from "./core/Banner";
|
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { CountBadge } from "./core/CountBadge";
|
import { CountBadge } from "./core/CountBadge";
|
||||||
import { HotkeyList } from "./core/HotkeyList";
|
import { HotkeyList } from "./core/HotkeyList";
|
||||||
import { HttpResponseDurationTag } from "./core/HttpResponseDurationTag";
|
import { HttpResponseDurationTag } from "./core/HttpResponseDurationTag";
|
||||||
import { HttpStatusTag } from "./core/HttpStatusTag";
|
import { HttpStatusTag } from "./core/HttpStatusTag";
|
||||||
import { Icon } from "./core/Icon";
|
|
||||||
import { LoadingIcon } from "./core/LoadingIcon";
|
|
||||||
import { PillButton } from "./core/PillButton";
|
import { PillButton } from "./core/PillButton";
|
||||||
import { SizeTag } from "./core/SizeTag";
|
import { SizeTag } from "./core/SizeTag";
|
||||||
import { HStack, VStack } from "./core/Stacks";
|
|
||||||
import type { TabItem } from "./core/Tabs/Tabs";
|
import type { TabItem } from "./core/Tabs/Tabs";
|
||||||
import { TabContent, Tabs } from "./core/Tabs/Tabs";
|
import { TabContent, Tabs } from "./core/Tabs/Tabs";
|
||||||
import { Tooltip } from "./core/Tooltip";
|
import { Tooltip } from "./core/Tooltip";
|
||||||
+40
-2
@@ -1,15 +1,20 @@
|
|||||||
import type {
|
import type {
|
||||||
|
AnyModel,
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
HttpResponseEvent,
|
HttpResponseEvent,
|
||||||
HttpResponseEventData,
|
HttpResponseEventData,
|
||||||
} from "@yaakapp-internal/models";
|
} from "@yaakapp-internal/models";
|
||||||
|
import { foldersAtom, workspacesAtom } from "@yaakapp-internal/models";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
import { type ReactNode, useMemo, useState } from "react";
|
import { type ReactNode, useMemo, useState } from "react";
|
||||||
import { useHttpResponseEvents } from "../hooks/useHttpResponseEvents";
|
import { useHttpResponseEvents } from "../hooks/useHttpResponseEvents";
|
||||||
|
import { useAllRequests } from "../hooks/useAllRequests";
|
||||||
|
import { resolvedModelName } from "../lib/resolvedModelName";
|
||||||
import { Editor } from "./core/Editor/LazyEditor";
|
import { Editor } from "./core/Editor/LazyEditor";
|
||||||
import { type EventDetailAction, EventDetailHeader, EventViewer } from "./core/EventViewer";
|
import { type EventDetailAction, EventDetailHeader, EventViewer } from "./core/EventViewer";
|
||||||
import { EventViewerRow } from "./core/EventViewerRow";
|
import { EventViewerRow } from "./core/EventViewerRow";
|
||||||
import { HttpStatusTagRaw } from "./core/HttpStatusTag";
|
import { HttpStatusTagRaw } from "./core/HttpStatusTag";
|
||||||
import { Icon, type IconProps } from "./core/Icon";
|
import { Icon, type IconProps } from "@yaakapp-internal/ui";
|
||||||
import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
|
import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
|
||||||
import type { TimelineViewMode } from "./HttpResponsePane";
|
import type { TimelineViewMode } from "./HttpResponsePane";
|
||||||
|
|
||||||
@@ -55,7 +60,7 @@ function Inner({ response, viewMode }: Props) {
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
loadingMessage="Loading events..."
|
loadingMessage="Loading events..."
|
||||||
emptyMessage="No events recorded"
|
emptyMessage="No events recorded"
|
||||||
splitLayoutName="http_response_events"
|
splitLayoutStorageKey="http_response_events"
|
||||||
defaultRatio={0.25}
|
defaultRatio={0.25}
|
||||||
renderRow={({ event, isActive, onClick }) => {
|
renderRow={({ event, isActive, onClick }) => {
|
||||||
const display = getEventDisplay(event.event);
|
const display = getEventDisplay(event.event);
|
||||||
@@ -95,6 +100,7 @@ function EventDetails({
|
|||||||
}) {
|
}) {
|
||||||
const { label } = getEventDisplay(event.event);
|
const { label } = getEventDisplay(event.event);
|
||||||
const e = event.event;
|
const e = event.event;
|
||||||
|
const settingSourceModels = useSettingSourceModels();
|
||||||
|
|
||||||
const actions: EventDetailAction[] = [
|
const actions: EventDetailAction[] = [
|
||||||
{
|
{
|
||||||
@@ -211,6 +217,9 @@ function EventDetails({
|
|||||||
<KeyValueRows>
|
<KeyValueRows>
|
||||||
<KeyValueRow label="Setting">{e.name}</KeyValueRow>
|
<KeyValueRow label="Setting">{e.name}</KeyValueRow>
|
||||||
<KeyValueRow label="Value">{e.value}</KeyValueRow>
|
<KeyValueRow label="Value">{e.value}</KeyValueRow>
|
||||||
|
{e.source_model != null ? (
|
||||||
|
<KeyValueRow label="Source">{formatSettingSource(e, settingSourceModels)}</KeyValueRow>
|
||||||
|
) : null}
|
||||||
</KeyValueRows>
|
</KeyValueRows>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -315,6 +324,35 @@ function formatEventText(event: HttpResponseEventData, includePrefix: boolean):
|
|||||||
return includePrefix ? `${prefix} ${text}` : text;
|
return includePrefix ? `${prefix} ${text}` : text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useSettingSourceModels() {
|
||||||
|
const requests = useAllRequests();
|
||||||
|
const folders = useAtomValue(foldersAtom);
|
||||||
|
const workspaces = useAtomValue(workspacesAtom);
|
||||||
|
|
||||||
|
return useMemo<AnyModel[]>(
|
||||||
|
() => [...requests, ...folders, ...workspaces],
|
||||||
|
[requests, folders, workspaces],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSettingSource(
|
||||||
|
event: Extract<HttpResponseEventData, { type: "setting" }>,
|
||||||
|
models: AnyModel[],
|
||||||
|
): string {
|
||||||
|
const sourceModel = event.source_model;
|
||||||
|
if (sourceModel == null || sourceModel === "default") {
|
||||||
|
return "Default";
|
||||||
|
}
|
||||||
|
|
||||||
|
const model =
|
||||||
|
event.source_id == null
|
||||||
|
? null
|
||||||
|
: (models.find((m) => m.model === sourceModel && m.id === event.source_id) ?? null);
|
||||||
|
const name = model == null ? event.source_name : resolvedModelName(model);
|
||||||
|
const label = sourceModel.replaceAll("_", " ");
|
||||||
|
return name == null || name.length === 0 ? label : `${name} (${label})`;
|
||||||
|
}
|
||||||
|
|
||||||
type EventDisplay = {
|
type EventDisplay = {
|
||||||
icon: IconProps["icon"];
|
icon: IconProps["icon"];
|
||||||
color: IconProps["color"];
|
color: IconProps["color"];
|
||||||
+3
-5
@@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { useImportCurl } from "../hooks/useImportCurl";
|
import { useImportCurl } from "../hooks/useImportCurl";
|
||||||
import { useWindowFocus } from "../hooks/useWindowFocus";
|
import { useWindowFocus } from "../hooks/useWindowFocus";
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { Icon } from "./core/Icon";
|
import { Icon } from "@yaakapp-internal/ui";
|
||||||
|
|
||||||
export function ImportCurlButton() {
|
export function ImportCurlButton() {
|
||||||
const focused = useWindowFocus();
|
const focused = useWindowFocus();
|
||||||
@@ -13,11 +13,9 @@ export function ImportCurlButton() {
|
|||||||
const importCurl = useImportCurl();
|
const importCurl = useImportCurl();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
// oxlint-disable-next-line react-hooks/exhaustive-deps -- none
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
readText()
|
void readText().then(setClipboardText);
|
||||||
.then(setClipboardText)
|
|
||||||
.catch(() => {});
|
|
||||||
}, [focused]);
|
}, [focused]);
|
||||||
|
|
||||||
if (!clipboardText?.trim().startsWith("curl ")) {
|
if (!clipboardText?.trim().startsWith("curl ")) {
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
|
import { VStack } from "@yaakapp-internal/ui";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useLocalStorage } from "react-use";
|
import { useLocalStorage } from "react-use";
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { VStack } from "./core/Stacks";
|
|
||||||
import { SelectFile } from "./SelectFile";
|
import { SelectFile } from "./SelectFile";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
+2
-3
@@ -1,17 +1,16 @@
|
|||||||
import { linter } from "@codemirror/lint";
|
import { linter } from "@codemirror/lint";
|
||||||
import type { HttpRequest } from "@yaakapp-internal/models";
|
import type { HttpRequest } from "@yaakapp-internal/models";
|
||||||
import { patchModel } from "@yaakapp-internal/models";
|
import { patchModel } from "@yaakapp-internal/models";
|
||||||
|
import { Banner, Icon } from "@yaakapp-internal/ui";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { fireAndForget } from "../lib/fireAndForget";
|
|
||||||
import { useKeyValue } from "../hooks/useKeyValue";
|
import { useKeyValue } from "../hooks/useKeyValue";
|
||||||
|
import { fireAndForget } from "../lib/fireAndForget";
|
||||||
import { textLikelyContainsJsonComments } from "../lib/jsonComments";
|
import { textLikelyContainsJsonComments } from "../lib/jsonComments";
|
||||||
import { Banner } from "./core/Banner";
|
|
||||||
import type { DropdownItem } from "./core/Dropdown";
|
import type { DropdownItem } from "./core/Dropdown";
|
||||||
import { Dropdown } from "./core/Dropdown";
|
import { Dropdown } from "./core/Dropdown";
|
||||||
import type { EditorProps } from "./core/Editor/Editor";
|
import type { EditorProps } from "./core/Editor/Editor";
|
||||||
import { jsonParseLinter } from "./core/Editor/json-lint";
|
import { jsonParseLinter } from "./core/Editor/json-lint";
|
||||||
import { Editor } from "./core/Editor/LazyEditor";
|
import { Editor } from "./core/Editor/LazyEditor";
|
||||||
import { Icon } from "./core/Icon";
|
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { IconTooltip } from "./core/IconTooltip";
|
import { IconTooltip } from "./core/IconTooltip";
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ import { jotaiStore } from "../lib/jotai";
|
|||||||
import { CargoFeature } from "./CargoFeature";
|
import { CargoFeature } from "./CargoFeature";
|
||||||
import type { ButtonProps } from "./core/Button";
|
import type { ButtonProps } from "./core/Button";
|
||||||
import { Dropdown, type DropdownItem } from "./core/Dropdown";
|
import { Dropdown, type DropdownItem } from "./core/Dropdown";
|
||||||
import { Icon } from "./core/Icon";
|
import { Icon } from "@yaakapp-internal/ui";
|
||||||
import { PillButton } from "./core/PillButton";
|
import { PillButton } from "./core/PillButton";
|
||||||
|
|
||||||
const dismissedAtom = atomWithKVStorage<string | null>("dismissed_license_expired", null);
|
const dismissedAtom = atomWithKVStorage<string | null>("dismissed_license_expired", null);
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
import type {
|
||||||
|
Folder,
|
||||||
|
GrpcRequest,
|
||||||
|
HttpRequest,
|
||||||
|
InheritedBoolSetting,
|
||||||
|
InheritedIntSetting,
|
||||||
|
WebsocketRequest,
|
||||||
|
Workspace,
|
||||||
|
} from "@yaakapp-internal/models";
|
||||||
|
import { patchModel } from "@yaakapp-internal/models";
|
||||||
|
import { useModelAncestors } from "../hooks/useModelAncestors";
|
||||||
|
import { Checkbox } from "./core/Checkbox";
|
||||||
|
import { PlainInput } from "./core/PlainInput";
|
||||||
|
import {
|
||||||
|
SettingOverrideRow,
|
||||||
|
SettingRowBoolean,
|
||||||
|
SettingRowNumber,
|
||||||
|
SettingsList,
|
||||||
|
SettingsSection,
|
||||||
|
} from "./core/SettingRow";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
showSectionTitles?: boolean;
|
||||||
|
model: ModelWithSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModelWithSettings = Workspace | Folder | HttpRequest | WebsocketRequest | GrpcRequest;
|
||||||
|
type ModelWithHttpSettings = Workspace | Folder | HttpRequest;
|
||||||
|
type ModelWithCookieSettings = Workspace | Folder | HttpRequest | WebsocketRequest | GrpcRequest;
|
||||||
|
type BooleanSetting = boolean | InheritedBoolSetting;
|
||||||
|
type IntegerSetting = number | InheritedIntSetting;
|
||||||
|
type CookieSettingsPatch = {
|
||||||
|
settingSendCookies?: ModelWithCookieSettings["settingSendCookies"];
|
||||||
|
settingStoreCookies?: ModelWithCookieSettings["settingStoreCookies"];
|
||||||
|
};
|
||||||
|
type HttpSettingsPatch = {
|
||||||
|
settingValidateCertificates?: ModelWithHttpSettings["settingValidateCertificates"];
|
||||||
|
settingFollowRedirects?: ModelWithHttpSettings["settingFollowRedirects"];
|
||||||
|
settingRequestTimeout?: ModelWithHttpSettings["settingRequestTimeout"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ModelSettingsEditor({ model, showSectionTitles = false }: Props) {
|
||||||
|
const ancestors = useModelAncestors(model);
|
||||||
|
const supportsHttpSettings =
|
||||||
|
model.model === "workspace" || model.model === "folder" || model.model === "http_request";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsList className="space-y-8">
|
||||||
|
{supportsHttpSettings && (
|
||||||
|
<SettingsSection title={showSectionTitles ? "Requests" : null}>
|
||||||
|
<IntegerSettingRow
|
||||||
|
title="Request Timeout"
|
||||||
|
description="Maximum request duration in milliseconds. Set to 0 to disable the timeout."
|
||||||
|
name="settingRequestTimeout"
|
||||||
|
setting={model.settingRequestTimeout}
|
||||||
|
inheritedValue={resolveInheritedValue(
|
||||||
|
ancestors,
|
||||||
|
"settingRequestTimeout",
|
||||||
|
model.settingRequestTimeout,
|
||||||
|
)}
|
||||||
|
onChange={(settingRequestTimeout) =>
|
||||||
|
patchHttpSettings(model, {
|
||||||
|
settingRequestTimeout,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<BooleanSettingRow
|
||||||
|
title="Validate TLS certificates"
|
||||||
|
description="When disabled, skip validation of server certificates."
|
||||||
|
setting={model.settingValidateCertificates}
|
||||||
|
inheritedValue={resolveInheritedValue(
|
||||||
|
ancestors,
|
||||||
|
"settingValidateCertificates",
|
||||||
|
model.settingValidateCertificates,
|
||||||
|
)}
|
||||||
|
onChange={(settingValidateCertificates) =>
|
||||||
|
patchHttpSettings(model, {
|
||||||
|
settingValidateCertificates,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<BooleanSettingRow
|
||||||
|
title="Follow redirects"
|
||||||
|
description="Follow HTTP redirects automatically."
|
||||||
|
setting={model.settingFollowRedirects}
|
||||||
|
inheritedValue={resolveInheritedValue(
|
||||||
|
ancestors,
|
||||||
|
"settingFollowRedirects",
|
||||||
|
model.settingFollowRedirects,
|
||||||
|
)}
|
||||||
|
onChange={(settingFollowRedirects) =>
|
||||||
|
patchHttpSettings(model, {
|
||||||
|
settingFollowRedirects,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SettingsSection>
|
||||||
|
)}
|
||||||
|
<SettingsSection title={supportsHttpSettings || showSectionTitles ? "Cookies" : null}>
|
||||||
|
<BooleanSettingRow
|
||||||
|
title="Automatically send cookies"
|
||||||
|
description="Attach matching cookies from the active cookie jar to outgoing requests."
|
||||||
|
setting={model.settingSendCookies}
|
||||||
|
inheritedValue={resolveInheritedValue(
|
||||||
|
ancestors,
|
||||||
|
"settingSendCookies",
|
||||||
|
model.settingSendCookies,
|
||||||
|
)}
|
||||||
|
onChange={(settingSendCookies) =>
|
||||||
|
patchCookieSettings(model, {
|
||||||
|
settingSendCookies,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<BooleanSettingRow
|
||||||
|
title="Automatically store cookies"
|
||||||
|
description="Save cookies from Set-Cookie response headers to the active cookie jar."
|
||||||
|
setting={model.settingStoreCookies}
|
||||||
|
inheritedValue={resolveInheritedValue(
|
||||||
|
ancestors,
|
||||||
|
"settingStoreCookies",
|
||||||
|
model.settingStoreCookies,
|
||||||
|
)}
|
||||||
|
onChange={(settingStoreCookies) =>
|
||||||
|
patchCookieSettings(model, {
|
||||||
|
settingStoreCookies,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SettingsSection>
|
||||||
|
</SettingsList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function countOverriddenSettings(model: ModelWithSettings) {
|
||||||
|
const settings: (BooleanSetting | IntegerSetting)[] = [
|
||||||
|
model.settingSendCookies,
|
||||||
|
model.settingStoreCookies,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (model.model === "workspace" || model.model === "folder" || model.model === "http_request") {
|
||||||
|
settings.push(
|
||||||
|
model.settingValidateCertificates,
|
||||||
|
model.settingFollowRedirects,
|
||||||
|
model.settingRequestTimeout,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings.filter((setting) => isInheritedSetting(setting) && setting.enabled === true)
|
||||||
|
.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchCookieSettings(model: ModelWithCookieSettings, patch: Partial<CookieSettingsPatch>) {
|
||||||
|
if (model.model === "workspace") return patchModel(model, patch as Partial<Workspace>);
|
||||||
|
if (model.model === "folder") return patchModel(model, patch as Partial<Folder>);
|
||||||
|
if (model.model === "http_request") return patchModel(model, patch as Partial<HttpRequest>);
|
||||||
|
if (model.model === "websocket_request")
|
||||||
|
return patchModel(model, patch as Partial<WebsocketRequest>);
|
||||||
|
return patchModel(model, patch as Partial<GrpcRequest>);
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchHttpSettings(model: ModelWithHttpSettings, patch: Partial<HttpSettingsPatch>) {
|
||||||
|
if (model.model === "workspace") return patchModel(model, patch as Partial<Workspace>);
|
||||||
|
if (model.model === "folder") return patchModel(model, patch as Partial<Folder>);
|
||||||
|
return patchModel(model, patch as Partial<HttpRequest>);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BooleanSettingRow({
|
||||||
|
description,
|
||||||
|
inheritedValue,
|
||||||
|
setting,
|
||||||
|
title,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
description: string;
|
||||||
|
inheritedValue: boolean;
|
||||||
|
setting: BooleanSetting;
|
||||||
|
title: string;
|
||||||
|
onChange: (setting: BooleanSetting) => void;
|
||||||
|
}) {
|
||||||
|
const inherited = isInheritedSetting(setting);
|
||||||
|
const overridden = inherited ? setting.enabled === true : false;
|
||||||
|
const value = inherited ? (overridden ? setting.value : inheritedValue) : setting;
|
||||||
|
|
||||||
|
if (!inherited) {
|
||||||
|
return (
|
||||||
|
<SettingRowBoolean
|
||||||
|
checked={value}
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
onChange={(value) => onChange(value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingOverrideRow
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
overridden={overridden}
|
||||||
|
onResetOverride={() => onChange({ ...setting, enabled: false })}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
hideLabel
|
||||||
|
size="md"
|
||||||
|
title={title}
|
||||||
|
checked={value}
|
||||||
|
onChange={(value) => onChange({ ...setting, enabled: true, value })}
|
||||||
|
/>
|
||||||
|
</SettingOverrideRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function IntegerSettingRow({
|
||||||
|
description,
|
||||||
|
inheritedValue,
|
||||||
|
name,
|
||||||
|
setting,
|
||||||
|
title,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
description: string;
|
||||||
|
inheritedValue: number;
|
||||||
|
name: string;
|
||||||
|
setting: IntegerSetting;
|
||||||
|
title: string;
|
||||||
|
onChange: (setting: IntegerSetting) => void;
|
||||||
|
}) {
|
||||||
|
const inherited = isInheritedSetting(setting);
|
||||||
|
const overridden = inherited ? setting.enabled === true : false;
|
||||||
|
const value = inherited ? (overridden ? setting.value : inheritedValue) : setting;
|
||||||
|
const showReset = overridden && value !== inheritedValue;
|
||||||
|
|
||||||
|
if (!inherited) {
|
||||||
|
return (
|
||||||
|
<SettingRowNumber
|
||||||
|
name={name}
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
value={value}
|
||||||
|
placeholder="0"
|
||||||
|
validate={(value) => value === "" || Number.parseInt(value, 10) >= 0}
|
||||||
|
onChange={(value) => onChange(value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingOverrideRow
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
overridden={showReset}
|
||||||
|
onResetOverride={() => onChange({ ...setting, enabled: false })}
|
||||||
|
>
|
||||||
|
<PlainInput
|
||||||
|
hideLabel
|
||||||
|
name={name}
|
||||||
|
label={title}
|
||||||
|
size="sm"
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
defaultValue={`${value}`}
|
||||||
|
containerClassName="!w-48"
|
||||||
|
validate={(value) => value === "" || Number.parseInt(value, 10) >= 0}
|
||||||
|
onChange={(value) =>
|
||||||
|
onChange({
|
||||||
|
...setting,
|
||||||
|
enabled: true,
|
||||||
|
value: Number.parseInt(value, 10) || 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SettingOverrideRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInheritedSetting<T>(
|
||||||
|
setting: T | { enabled?: boolean; value: T },
|
||||||
|
): setting is { enabled?: boolean; value: T } {
|
||||||
|
return typeof setting === "object" && setting != null && "value" in setting;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveInheritedValue(
|
||||||
|
ancestors: (Folder | Workspace)[],
|
||||||
|
key: "settingRequestTimeout",
|
||||||
|
fallback: IntegerSetting,
|
||||||
|
): number;
|
||||||
|
function resolveInheritedValue(
|
||||||
|
ancestors: (Folder | Workspace)[],
|
||||||
|
key: BooleanWorkspaceSettingKey,
|
||||||
|
fallback: BooleanSetting,
|
||||||
|
): boolean;
|
||||||
|
function resolveInheritedValue(
|
||||||
|
ancestors: (Folder | Workspace)[],
|
||||||
|
key: keyof WorkspaceSettings,
|
||||||
|
fallback: BooleanSetting | IntegerSetting,
|
||||||
|
) {
|
||||||
|
for (const ancestor of ancestors) {
|
||||||
|
const setting = ancestor[key] as BooleanSetting | IntegerSetting;
|
||||||
|
if (isInheritedSetting(setting)) {
|
||||||
|
if (setting.enabled === true) {
|
||||||
|
return setting.value;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return setting;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isInheritedSetting(fallback) ? fallback.value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkspaceSettings = Pick<
|
||||||
|
Workspace,
|
||||||
|
| "settingFollowRedirects"
|
||||||
|
| "settingRequestTimeout"
|
||||||
|
| "settingSendCookies"
|
||||||
|
| "settingStoreCookies"
|
||||||
|
| "settingValidateCertificates"
|
||||||
|
>;
|
||||||
|
|
||||||
|
type BooleanWorkspaceSettingKey = Exclude<keyof WorkspaceSettings, "settingRequestTimeout">;
|
||||||
+1
-2
@@ -1,5 +1,6 @@
|
|||||||
import type { GrpcRequest, HttpRequest, WebsocketRequest } from "@yaakapp-internal/models";
|
import type { GrpcRequest, HttpRequest, WebsocketRequest } from "@yaakapp-internal/models";
|
||||||
import { patchModel, workspacesAtom } from "@yaakapp-internal/models";
|
import { patchModel, workspacesAtom } from "@yaakapp-internal/models";
|
||||||
|
import { InlineCode, VStack } from "@yaakapp-internal/ui";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { pluralizeCount } from "../lib/pluralize";
|
import { pluralizeCount } from "../lib/pluralize";
|
||||||
@@ -7,9 +8,7 @@ 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";
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { InlineCode } from "./core/InlineCode";
|
|
||||||
import { Select } from "./core/Select";
|
import { Select } from "./core/Select";
|
||||||
import { VStack } from "./core/Stacks";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
activeWorkspaceId: string;
|
activeWorkspaceId: string;
|
||||||
+1
-2
@@ -1,12 +1,11 @@
|
|||||||
import type { GrpcConnection } from "@yaakapp-internal/models";
|
import type { GrpcConnection } from "@yaakapp-internal/models";
|
||||||
import { deleteModel } from "@yaakapp-internal/models";
|
import { deleteModel } from "@yaakapp-internal/models";
|
||||||
|
import { HStack, Icon } from "@yaakapp-internal/ui";
|
||||||
import { formatDistanceToNowStrict } from "date-fns";
|
import { formatDistanceToNowStrict } from "date-fns";
|
||||||
import { useDeleteGrpcConnections } from "../hooks/useDeleteGrpcConnections";
|
import { useDeleteGrpcConnections } from "../hooks/useDeleteGrpcConnections";
|
||||||
import { pluralizeCount } from "../lib/pluralize";
|
import { pluralizeCount } from "../lib/pluralize";
|
||||||
import { Dropdown } from "./core/Dropdown";
|
import { Dropdown } from "./core/Dropdown";
|
||||||
import { Icon } from "./core/Icon";
|
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { HStack } from "./core/Stacks";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
connections: GrpcConnection[];
|
connections: GrpcConnection[];
|
||||||
+1
-2
@@ -1,14 +1,13 @@
|
|||||||
import type { HttpResponse } from "@yaakapp-internal/models";
|
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||||
import { deleteModel } from "@yaakapp-internal/models";
|
import { deleteModel } from "@yaakapp-internal/models";
|
||||||
|
import { HStack, Icon } from "@yaakapp-internal/ui";
|
||||||
import { useCopyHttpResponse } from "../hooks/useCopyHttpResponse";
|
import { useCopyHttpResponse } from "../hooks/useCopyHttpResponse";
|
||||||
import { useDeleteHttpResponses } from "../hooks/useDeleteHttpResponses";
|
import { useDeleteHttpResponses } from "../hooks/useDeleteHttpResponses";
|
||||||
import { useSaveResponse } from "../hooks/useSaveResponse";
|
import { useSaveResponse } from "../hooks/useSaveResponse";
|
||||||
import { pluralize } from "../lib/pluralize";
|
import { pluralize } from "../lib/pluralize";
|
||||||
import { Dropdown } from "./core/Dropdown";
|
import { Dropdown } from "./core/Dropdown";
|
||||||
import { HttpStatusTag } from "./core/HttpStatusTag";
|
import { HttpStatusTag } from "./core/HttpStatusTag";
|
||||||
import { Icon } from "./core/Icon";
|
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { HStack } from "./core/Stacks";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
responses: HttpResponse[];
|
responses: HttpResponse[];
|
||||||
+1
-2
@@ -1,12 +1,11 @@
|
|||||||
import type { WebsocketConnection } from "@yaakapp-internal/models";
|
import type { WebsocketConnection } from "@yaakapp-internal/models";
|
||||||
import { deleteModel, getModel } from "@yaakapp-internal/models";
|
import { deleteModel, getModel } from "@yaakapp-internal/models";
|
||||||
|
import { HStack, Icon } from "@yaakapp-internal/ui";
|
||||||
import { formatDistanceToNowStrict } from "date-fns";
|
import { formatDistanceToNowStrict } from "date-fns";
|
||||||
import { deleteWebsocketConnections } from "../commands/deleteWebsocketConnections";
|
import { deleteWebsocketConnections } from "../commands/deleteWebsocketConnections";
|
||||||
import { pluralizeCount } from "../lib/pluralize";
|
import { pluralizeCount } from "../lib/pluralize";
|
||||||
import { Dropdown } from "./core/Dropdown";
|
import { Dropdown } from "./core/Dropdown";
|
||||||
import { Icon } from "./core/Icon";
|
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { HStack } from "./core/Stacks";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
connections: WebsocketConnection[];
|
connections: WebsocketConnection[];
|
||||||
+1
-1
@@ -2,7 +2,7 @@ import type { HttpResponse } from "@yaakapp-internal/models";
|
|||||||
import { lazy, Suspense } from "react";
|
import { lazy, Suspense } from "react";
|
||||||
import { useHttpRequestBody } from "../hooks/useHttpRequestBody";
|
import { useHttpRequestBody } from "../hooks/useHttpRequestBody";
|
||||||
import { getMimeTypeFromContentType, languageFromContentType } from "../lib/contentType";
|
import { getMimeTypeFromContentType, languageFromContentType } from "../lib/contentType";
|
||||||
import { LoadingIcon } from "./core/LoadingIcon";
|
import { LoadingIcon } from "@yaakapp-internal/ui";
|
||||||
import { EmptyStateText } from "./EmptyStateText";
|
import { EmptyStateText } from "./EmptyStateText";
|
||||||
import { AudioViewer } from "./responseViewers/AudioViewer";
|
import { AudioViewer } from "./responseViewers/AudioViewer";
|
||||||
import { CsvViewer } from "./responseViewers/CsvViewer";
|
import { CsvViewer } from "./responseViewers/CsvViewer";
|
||||||
+1
-1
@@ -6,7 +6,7 @@ import { showPrompt } from "../lib/prompt";
|
|||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import type { DropdownItem } from "./core/Dropdown";
|
import type { DropdownItem } from "./core/Dropdown";
|
||||||
import { HttpMethodTag, HttpMethodTagRaw } from "./core/HttpMethodTag";
|
import { HttpMethodTag, HttpMethodTagRaw } from "./core/HttpMethodTag";
|
||||||
import { Icon } from "./core/Icon";
|
import { Icon } from "@yaakapp-internal/ui";
|
||||||
import type { RadioDropdownItem } from "./core/RadioDropdown";
|
import type { RadioDropdownItem } from "./core/RadioDropdown";
|
||||||
import { RadioDropdown } from "./core/RadioDropdown";
|
import { RadioDropdown } from "./core/RadioDropdown";
|
||||||
|
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
import { Button } from "./core/Button";
|
import { Button, FormattedError, Heading, VStack } from "@yaakapp-internal/ui";
|
||||||
import { DetailsBanner } from "./core/DetailsBanner";
|
import { DetailsBanner } from "./core/DetailsBanner";
|
||||||
import { FormattedError } from "./core/FormattedError";
|
|
||||||
import { Heading } from "./core/Heading";
|
|
||||||
import { VStack } from "./core/Stacks";
|
|
||||||
|
|
||||||
export default function RouteError({ error }: { error: unknown }) {
|
export default function RouteError({ error }: { error: unknown }) {
|
||||||
console.log("Error", error);
|
console.log("Error", error);
|
||||||
const stringified = JSON.stringify(error);
|
const stringified = JSON.stringify(error);
|
||||||
// oxlint-disable-next-line no-explicit-any
|
// oxlint-disable-next-line no-explicit-any -- none
|
||||||
const message = (error as any).message ?? stringified;
|
const message = (error as any).message ?? stringified;
|
||||||
const stack =
|
const stack =
|
||||||
typeof error === "object" && error != null && "stack" in error ? String(error.stack) : null;
|
typeof error === "object" && error != null && "stack" in error ? String(error.stack) : null;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
|
import { HStack } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import mime from "mime";
|
import mime from "mime";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
@@ -9,7 +10,6 @@ import { Button } from "./core/Button";
|
|||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { IconTooltip } from "./core/IconTooltip";
|
import { IconTooltip } from "./core/IconTooltip";
|
||||||
import { Label } from "./core/Label";
|
import { Label } from "./core/Label";
|
||||||
import { HStack } from "./core/Stacks";
|
|
||||||
|
|
||||||
type Props = Omit<ButtonProps, "type"> & {
|
type Props = Omit<ButtonProps, "type"> & {
|
||||||
onChange: (value: { filePath: string | null; contentType: string | null }) => void;
|
onChange: (value: { filePath: string | null; contentType: string | null }) => void;
|
||||||
@@ -19,6 +19,7 @@ type Props = Omit<ButtonProps, "type"> & {
|
|||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
noun?: string;
|
noun?: string;
|
||||||
help?: ReactNode;
|
help?: ReactNode;
|
||||||
|
hideLabel?: boolean;
|
||||||
label?: ReactNode;
|
label?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ export function SelectFile({
|
|||||||
size = "sm",
|
size = "sm",
|
||||||
label,
|
label,
|
||||||
help,
|
help,
|
||||||
|
hideLabel,
|
||||||
...props
|
...props
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
@@ -95,7 +97,7 @@ export function SelectFile({
|
|||||||
return (
|
return (
|
||||||
<div ref={ref} className="w-full">
|
<div ref={ref} className="w-full">
|
||||||
{label && (
|
{label && (
|
||||||
<Label htmlFor={null} help={help}>
|
<Label htmlFor={null} help={help} visuallyHidden={hideLabel}>
|
||||||
{label}
|
{label}
|
||||||
</Label>
|
</Label>
|
||||||
)}
|
)}
|
||||||
+5
-3
@@ -3,16 +3,14 @@ import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
|||||||
import { type } from "@tauri-apps/plugin-os";
|
import { type } from "@tauri-apps/plugin-os";
|
||||||
import { useLicense } from "@yaakapp-internal/license";
|
import { useLicense } from "@yaakapp-internal/license";
|
||||||
import { pluginsAtom, settingsAtom } from "@yaakapp-internal/models";
|
import { pluginsAtom, settingsAtom } from "@yaakapp-internal/models";
|
||||||
|
import { HeaderSize, HStack, Icon } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useKeyPressEvent } from "react-use";
|
import { useKeyPressEvent } from "react-use";
|
||||||
import { appInfo } from "../../lib/appInfo";
|
import { appInfo } from "../../lib/appInfo";
|
||||||
import { capitalize } from "../../lib/capitalize";
|
import { capitalize } from "../../lib/capitalize";
|
||||||
import { CountBadge } from "../core/CountBadge";
|
import { CountBadge } from "../core/CountBadge";
|
||||||
import { Icon } from "../core/Icon";
|
|
||||||
import { HStack } from "../core/Stacks";
|
|
||||||
import { TabContent, type TabItem, Tabs } from "../core/Tabs/Tabs";
|
import { TabContent, type TabItem, Tabs } from "../core/Tabs/Tabs";
|
||||||
import { HeaderSize } from "../HeaderSize";
|
|
||||||
import { SettingsCertificates } from "./SettingsCertificates";
|
import { SettingsCertificates } from "./SettingsCertificates";
|
||||||
import { SettingsGeneral } from "./SettingsGeneral";
|
import { SettingsGeneral } from "./SettingsGeneral";
|
||||||
import { SettingsHotkeys } from "./SettingsHotkeys";
|
import { SettingsHotkeys } from "./SettingsHotkeys";
|
||||||
@@ -77,6 +75,10 @@ export default function Settings({ hide }: Props) {
|
|||||||
onlyXWindowControl
|
onlyXWindowControl
|
||||||
size="md"
|
size="md"
|
||||||
className="x-theme-appHeader bg-surface text-text-subtle flex items-center justify-center border-b border-border-subtle text-sm font-semibold"
|
className="x-theme-appHeader bg-surface text-text-subtle flex items-center justify-center border-b border-border-subtle text-sm font-semibold"
|
||||||
|
osType={type()}
|
||||||
|
hideWindowControls={settings.hideWindowControls}
|
||||||
|
useNativeTitlebar={settings.useNativeTitlebar}
|
||||||
|
interfaceScale={settings.interfaceScale}
|
||||||
>
|
>
|
||||||
<HStack
|
<HStack
|
||||||
space={2}
|
space={2}
|
||||||
+1
-3
@@ -1,17 +1,15 @@
|
|||||||
import type { ClientCertificate } from "@yaakapp-internal/models";
|
import type { ClientCertificate } from "@yaakapp-internal/models";
|
||||||
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
|
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
|
||||||
|
import { Heading, HStack, InlineCode, VStack } from "@yaakapp-internal/ui";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { showConfirmDelete } from "../../lib/confirm";
|
import { showConfirmDelete } from "../../lib/confirm";
|
||||||
import { Button } from "../core/Button";
|
import { Button } from "../core/Button";
|
||||||
import { Checkbox } from "../core/Checkbox";
|
import { Checkbox } from "../core/Checkbox";
|
||||||
import { DetailsBanner } from "../core/DetailsBanner";
|
import { DetailsBanner } from "../core/DetailsBanner";
|
||||||
import { Heading } from "../core/Heading";
|
|
||||||
import { IconButton } from "../core/IconButton";
|
import { IconButton } from "../core/IconButton";
|
||||||
import { InlineCode } from "../core/InlineCode";
|
|
||||||
import { PlainInput } from "../core/PlainInput";
|
import { PlainInput } from "../core/PlainInput";
|
||||||
import { Separator } from "../core/Separator";
|
import { Separator } from "../core/Separator";
|
||||||
import { HStack, VStack } from "../core/Stacks";
|
|
||||||
import { SelectFile } from "../SelectFile";
|
import { SelectFile } from "../SelectFile";
|
||||||
|
|
||||||
function createEmptyCertificate(): ClientCertificate {
|
function createEmptyCertificate(): ClientCertificate {
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
import { revealItemInDir } from "@tauri-apps/plugin-opener";
|
||||||
|
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
|
||||||
|
import { Heading, VStack } from "@yaakapp-internal/ui";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
|
||||||
|
import { useCheckForUpdates } from "../../hooks/useCheckForUpdates";
|
||||||
|
import { appInfo } from "../../lib/appInfo";
|
||||||
|
import { revealInFinderText } from "../../lib/reveal";
|
||||||
|
import { CargoFeature } from "../CargoFeature";
|
||||||
|
import { IconButton } from "../core/IconButton";
|
||||||
|
import {
|
||||||
|
ModelSettingRowBoolean,
|
||||||
|
ModelSettingRowNumber,
|
||||||
|
ModelSettingSelectControl,
|
||||||
|
SettingValue,
|
||||||
|
SettingRow,
|
||||||
|
SettingRowBoolean,
|
||||||
|
SettingRowSelect,
|
||||||
|
SettingsList,
|
||||||
|
SettingsSection,
|
||||||
|
} from "../core/SettingRow";
|
||||||
|
|
||||||
|
export function SettingsGeneral() {
|
||||||
|
const workspace = useAtomValue(activeWorkspaceAtom);
|
||||||
|
const settings = useAtomValue(settingsAtom);
|
||||||
|
const checkForUpdates = useCheckForUpdates();
|
||||||
|
|
||||||
|
if (settings == null || workspace == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack space={1.5} className="mb-4">
|
||||||
|
<div className="mb-4">
|
||||||
|
<Heading>General</Heading>
|
||||||
|
<p className="text-text-subtle">Configure general settings for update behavior and more.</p>
|
||||||
|
</div>
|
||||||
|
<SettingsList className="space-y-8">
|
||||||
|
<CargoFeature feature="updater">
|
||||||
|
<SettingsSection title="Updates">
|
||||||
|
<SettingRow
|
||||||
|
title="Update Channel"
|
||||||
|
description="Choose whether Yaak should use stable releases or beta releases."
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-[12rem_auto] gap-1">
|
||||||
|
<ModelSettingSelectControl
|
||||||
|
model={settings}
|
||||||
|
modelKey="updateChannel"
|
||||||
|
label="Update Channel"
|
||||||
|
selectClassName="!w-full"
|
||||||
|
options={[
|
||||||
|
{ label: "Stable", value: "stable" },
|
||||||
|
{ label: "Beta", value: "beta" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
variant="border"
|
||||||
|
size="sm"
|
||||||
|
title="Check for updates"
|
||||||
|
icon="refresh"
|
||||||
|
spin={checkForUpdates.isPending}
|
||||||
|
onClick={() => checkForUpdates.mutateAsync()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingRow>
|
||||||
|
|
||||||
|
<SettingRowSelect
|
||||||
|
title="Update Behavior"
|
||||||
|
description="Choose whether updates are installed automatically or manually."
|
||||||
|
name="autoupdate"
|
||||||
|
value={settings.autoupdate ? "auto" : "manual"}
|
||||||
|
onChange={(v) => patchModel(settings, { autoupdate: v === "auto" })}
|
||||||
|
options={[
|
||||||
|
{ label: "Automatic", value: "auto" },
|
||||||
|
{ label: "Manual", value: "manual" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModelSettingRowBoolean
|
||||||
|
model={settings}
|
||||||
|
modelKey="autoDownloadUpdates"
|
||||||
|
title="Automatically download updates"
|
||||||
|
description="Download Yaak updates in the background so they are ready to install."
|
||||||
|
disabled={!settings.autoupdate}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModelSettingRowBoolean
|
||||||
|
model={settings}
|
||||||
|
modelKey="checkNotifications"
|
||||||
|
title="Check for notifications"
|
||||||
|
description="Periodically ping Yaak servers to check for relevant notifications."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingRowBoolean
|
||||||
|
title="Send anonymous usage statistics"
|
||||||
|
description="Yaak is local-first and does not collect analytics or usage data."
|
||||||
|
disabled
|
||||||
|
checked={false}
|
||||||
|
onChange={() => {}}
|
||||||
|
/>
|
||||||
|
</SettingsSection>
|
||||||
|
</CargoFeature>
|
||||||
|
|
||||||
|
<SettingsSection
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Workspace{" "}
|
||||||
|
<span className="inline-block bg-surface-highlight px-2 py-0.5 rounded text">
|
||||||
|
{workspace.name}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ModelSettingRowNumber
|
||||||
|
model={workspace}
|
||||||
|
modelKey="settingRequestTimeout"
|
||||||
|
title="Request Timeout"
|
||||||
|
description="Maximum request duration in milliseconds. Set to 0 to disable the timeout."
|
||||||
|
placeholder="0"
|
||||||
|
required
|
||||||
|
validate={(value) => Number.parseInt(value, 10) >= 0}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModelSettingRowBoolean
|
||||||
|
model={workspace}
|
||||||
|
modelKey="settingValidateCertificates"
|
||||||
|
title="Validate TLS certificates"
|
||||||
|
description="When disabled, skip validation of server certificates."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModelSettingRowBoolean
|
||||||
|
model={workspace}
|
||||||
|
modelKey="settingFollowRedirects"
|
||||||
|
title="Follow redirects"
|
||||||
|
description="Follow HTTP redirects automatically."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModelSettingRowBoolean
|
||||||
|
model={workspace}
|
||||||
|
modelKey="settingSendCookies"
|
||||||
|
title="Automatically send cookies"
|
||||||
|
description="Attach matching cookies from the active cookie jar to outgoing requests."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModelSettingRowBoolean
|
||||||
|
model={workspace}
|
||||||
|
modelKey="settingStoreCookies"
|
||||||
|
title="Automatically store cookies"
|
||||||
|
description="Save cookies from Set-Cookie response headers to the active cookie jar."
|
||||||
|
/>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
|
<SettingsSection title="App Info">
|
||||||
|
<SettingRow title="Version" description="Current Yaak version.">
|
||||||
|
<SettingValue value={appInfo.version} />
|
||||||
|
</SettingRow>
|
||||||
|
<SettingRow
|
||||||
|
title="Data Directory"
|
||||||
|
description="Where Yaak stores application data."
|
||||||
|
controlClassName="min-w-0 max-w-[min(42rem,55vw)] gap-2"
|
||||||
|
>
|
||||||
|
<SettingValue
|
||||||
|
value={appInfo.appDataDir}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
title: revealInFinderText,
|
||||||
|
icon: "folder_open",
|
||||||
|
onClick: () => revealItemInDir(appInfo.appDataDir),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingRow
|
||||||
|
title="Logs Directory"
|
||||||
|
description="Where Yaak writes application logs."
|
||||||
|
controlClassName="min-w-0 max-w-[min(42rem,55vw)] gap-2"
|
||||||
|
>
|
||||||
|
<SettingValue
|
||||||
|
value={appInfo.appLogDir}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
title: revealInFinderText,
|
||||||
|
icon: "folder_open",
|
||||||
|
onClick: () => revealItemInDir(appInfo.appLogDir),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
</SettingsSection>
|
||||||
|
</SettingsList>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
+12
-4
@@ -1,4 +1,16 @@
|
|||||||
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
|
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
|
||||||
|
import {
|
||||||
|
Heading,
|
||||||
|
HStack,
|
||||||
|
Icon,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableRow,
|
||||||
|
VStack,
|
||||||
|
} from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { fuzzyMatch } from "fuzzbunny";
|
import { fuzzyMatch } from "fuzzbunny";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
@@ -16,13 +28,9 @@ import { capitalize } from "../../lib/capitalize";
|
|||||||
import { showDialog } from "../../lib/dialog";
|
import { showDialog } from "../../lib/dialog";
|
||||||
import { Button } from "../core/Button";
|
import { Button } from "../core/Button";
|
||||||
import { Dropdown, type DropdownItem } from "../core/Dropdown";
|
import { Dropdown, type DropdownItem } from "../core/Dropdown";
|
||||||
import { Heading } from "../core/Heading";
|
|
||||||
import { HotkeyRaw } from "../core/Hotkey";
|
import { HotkeyRaw } from "../core/Hotkey";
|
||||||
import { Icon } from "../core/Icon";
|
|
||||||
import { IconButton } from "../core/IconButton";
|
import { IconButton } from "../core/IconButton";
|
||||||
import { PlainInput } from "../core/PlainInput";
|
import { PlainInput } from "../core/PlainInput";
|
||||||
import { HStack, VStack } from "../core/Stacks";
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "../core/Table";
|
|
||||||
|
|
||||||
const HOLD_KEYS = ["Shift", "Control", "Alt", "Meta"];
|
const HOLD_KEYS = ["Shift", "Control", "Alt", "Meta"];
|
||||||
const LAYOUT_INSENSITIVE_KEYS = [
|
const LAYOUT_INSENSITIVE_KEYS = [
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
import { type } from "@tauri-apps/plugin-os";
|
||||||
|
import { useFonts } from "@yaakapp-internal/fonts";
|
||||||
|
import { useLicense } from "@yaakapp-internal/license";
|
||||||
|
import type { EditorKeymap, Settings } from "@yaakapp-internal/models";
|
||||||
|
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
|
||||||
|
import { clamp, Heading, VStack } from "@yaakapp-internal/ui";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
|
||||||
|
import { showConfirm } from "../../lib/confirm";
|
||||||
|
import { invokeCmd } from "../../lib/tauri";
|
||||||
|
import { CargoFeature } from "../CargoFeature";
|
||||||
|
import { Button } from "../core/Button";
|
||||||
|
import { Checkbox } from "../core/Checkbox";
|
||||||
|
import { Link } from "../core/Link";
|
||||||
|
import {
|
||||||
|
ModelSettingRowBoolean,
|
||||||
|
ModelSettingRowSelect,
|
||||||
|
SettingRow,
|
||||||
|
SettingRowBoolean,
|
||||||
|
SettingRowSelect,
|
||||||
|
SettingSelectControl,
|
||||||
|
SettingsList,
|
||||||
|
SettingsSection,
|
||||||
|
} from "../core/SettingRow";
|
||||||
|
|
||||||
|
const NULL_FONT_VALUE = "__NULL_FONT__";
|
||||||
|
|
||||||
|
const fontSizeOptions = [
|
||||||
|
8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
|
||||||
|
].map((n) => ({ label: `${n}`, value: `${n}` }));
|
||||||
|
|
||||||
|
const keymaps: { value: EditorKeymap; label: string }[] = [
|
||||||
|
{ value: "default", label: "Default" },
|
||||||
|
{ value: "vim", label: "Vim" },
|
||||||
|
{ value: "vscode", label: "VSCode" },
|
||||||
|
{ value: "emacs", label: "Emacs" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function SettingsInterface() {
|
||||||
|
const workspace = useAtomValue(activeWorkspaceAtom);
|
||||||
|
const settings = useAtomValue(settingsAtom);
|
||||||
|
const fonts = useFonts();
|
||||||
|
|
||||||
|
if (settings == null || workspace == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack space={1.5} className="mb-4">
|
||||||
|
<div className="mb-3">
|
||||||
|
<Heading>Interface</Heading>
|
||||||
|
<p className="text-text-subtle">Tweak settings related to the user interface.</p>
|
||||||
|
</div>
|
||||||
|
<SettingsList className="space-y-8">
|
||||||
|
<SettingsSection title="Workspaces">
|
||||||
|
<SettingRowSelect
|
||||||
|
title="Open workspace behavior"
|
||||||
|
description="Choose what happens when opening another workspace."
|
||||||
|
name="switchWorkspaceBehavior"
|
||||||
|
value={
|
||||||
|
settings.openWorkspaceNewWindow === true
|
||||||
|
? "new"
|
||||||
|
: settings.openWorkspaceNewWindow === false
|
||||||
|
? "current"
|
||||||
|
: "ask"
|
||||||
|
}
|
||||||
|
onChange={async (v) => {
|
||||||
|
if (v === "current") await patchModel(settings, { openWorkspaceNewWindow: false });
|
||||||
|
else if (v === "new") await patchModel(settings, { openWorkspaceNewWindow: true });
|
||||||
|
else await patchModel(settings, { openWorkspaceNewWindow: null });
|
||||||
|
}}
|
||||||
|
options={[
|
||||||
|
{ label: "Always ask", value: "ask" },
|
||||||
|
{ label: "Open in current window", value: "current" },
|
||||||
|
{ label: "Open in new window", value: "new" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
|
<SettingsSection title="Fonts">
|
||||||
|
<SettingRow
|
||||||
|
title="Interface font"
|
||||||
|
description="Font used for Yaak interface controls."
|
||||||
|
controlClassName="gap-1"
|
||||||
|
>
|
||||||
|
{fonts.data && (
|
||||||
|
<SettingSelectControl
|
||||||
|
name="uiFont"
|
||||||
|
label="Interface font"
|
||||||
|
selectClassName="!w-72"
|
||||||
|
value={settings.interfaceFont ?? NULL_FONT_VALUE}
|
||||||
|
defaultValue={NULL_FONT_VALUE}
|
||||||
|
options={[
|
||||||
|
{ label: "System default", value: NULL_FONT_VALUE },
|
||||||
|
...fonts.data.uiFonts.map((f) => ({ label: f, value: f })),
|
||||||
|
...fonts.data.editorFonts.map((f) => ({ label: f, value: f })),
|
||||||
|
]}
|
||||||
|
onChange={async (v) => {
|
||||||
|
const interfaceFont = v === NULL_FONT_VALUE ? null : v;
|
||||||
|
await patchModel(settings, { interfaceFont });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<SettingSelectControl
|
||||||
|
name="interfaceFontSize"
|
||||||
|
label="Interface Font Size"
|
||||||
|
selectClassName="!w-20"
|
||||||
|
value={`${settings.interfaceFontSize}`}
|
||||||
|
defaultValue="14"
|
||||||
|
options={fontSizeOptions}
|
||||||
|
onChange={(v) => patchModel(settings, { interfaceFontSize: Number.parseInt(v, 10) })}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
|
||||||
|
<SettingRow
|
||||||
|
title="Editor font"
|
||||||
|
description="Font used in request and response editors."
|
||||||
|
controlClassName="gap-1"
|
||||||
|
>
|
||||||
|
{fonts.data && (
|
||||||
|
<SettingSelectControl
|
||||||
|
name="editorFont"
|
||||||
|
label="Editor font"
|
||||||
|
selectClassName="!w-72"
|
||||||
|
value={settings.editorFont ?? NULL_FONT_VALUE}
|
||||||
|
defaultValue={NULL_FONT_VALUE}
|
||||||
|
options={[
|
||||||
|
{ label: "System default", value: NULL_FONT_VALUE },
|
||||||
|
...fonts.data.editorFonts.map((f) => ({ label: f, value: f })),
|
||||||
|
]}
|
||||||
|
onChange={async (v) => {
|
||||||
|
const editorFont = v === NULL_FONT_VALUE ? null : v;
|
||||||
|
await patchModel(settings, { editorFont });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<SettingSelectControl
|
||||||
|
name="editorFontSize"
|
||||||
|
label="Editor Font Size"
|
||||||
|
selectClassName="!w-20"
|
||||||
|
value={`${settings.editorFontSize}`}
|
||||||
|
defaultValue="12"
|
||||||
|
options={fontSizeOptions}
|
||||||
|
onChange={(v) =>
|
||||||
|
patchModel(settings, {
|
||||||
|
editorFontSize: clamp(Number.parseInt(v, 10) || 14, 8, 30),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
|
<SettingsSection title="Editor">
|
||||||
|
<ModelSettingRowSelect
|
||||||
|
model={settings}
|
||||||
|
modelKey="editorKeymap"
|
||||||
|
title="Editor keymap"
|
||||||
|
description="Keyboard shortcut preset used by text editors."
|
||||||
|
options={keymaps}
|
||||||
|
/>
|
||||||
|
<ModelSettingRowBoolean
|
||||||
|
model={settings}
|
||||||
|
modelKey="editorSoftWrap"
|
||||||
|
title="Wrap editor lines"
|
||||||
|
description="Wrap long lines in request and response editors."
|
||||||
|
/>
|
||||||
|
<ModelSettingRowBoolean
|
||||||
|
model={settings}
|
||||||
|
modelKey="coloredMethods"
|
||||||
|
title="Colorize request methods"
|
||||||
|
description="Use method-specific colors for HTTP request methods."
|
||||||
|
/>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
|
<SettingsSection title="Window">
|
||||||
|
<NativeTitlebarSetting settings={settings} />
|
||||||
|
{type() !== "macos" && (
|
||||||
|
<ModelSettingRowBoolean
|
||||||
|
model={settings}
|
||||||
|
modelKey="hideWindowControls"
|
||||||
|
title="Hide window controls"
|
||||||
|
description="Hide the close, maximize, and minimize controls on Windows or Linux."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
|
<CargoFeature feature="license">
|
||||||
|
<LicenseSettings settings={settings} />
|
||||||
|
</CargoFeature>
|
||||||
|
</SettingsList>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NativeTitlebarSetting({ settings }: { settings: Settings }) {
|
||||||
|
const [nativeTitlebar, setNativeTitlebar] = useState(settings.useNativeTitlebar);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingRow
|
||||||
|
title="Native title bar"
|
||||||
|
description="Use the operating system's standard title bar and window controls."
|
||||||
|
controlClassName="gap-2"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
hideLabel
|
||||||
|
size="md"
|
||||||
|
checked={nativeTitlebar}
|
||||||
|
title="Native title bar"
|
||||||
|
onChange={setNativeTitlebar}
|
||||||
|
/>
|
||||||
|
{settings.useNativeTitlebar !== nativeTitlebar && (
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
size="xs"
|
||||||
|
onClick={async () => {
|
||||||
|
await patchModel(settings, { useNativeTitlebar: nativeTitlebar });
|
||||||
|
await invokeCmd("cmd_restart");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Apply and Restart
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</SettingRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LicenseSettings({ settings }: { settings: Settings }) {
|
||||||
|
const license = useLicense();
|
||||||
|
if (license.check.data?.status !== "personal_use") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsSection title="License">
|
||||||
|
<SettingRowBoolean
|
||||||
|
checked={settings.hideLicenseBadge}
|
||||||
|
title="Hide personal use badge"
|
||||||
|
description="Hide the personal-use badge from the interface."
|
||||||
|
onChange={async (hideLicenseBadge) => {
|
||||||
|
if (hideLicenseBadge) {
|
||||||
|
const confirmed = await showConfirm({
|
||||||
|
id: "hide-license-badge",
|
||||||
|
title: "Confirm Personal Use",
|
||||||
|
confirmText: "Confirm",
|
||||||
|
description: (
|
||||||
|
<VStack space={3}>
|
||||||
|
<p>Hey there 👋🏼</p>
|
||||||
|
<p>
|
||||||
|
Yaak is free for personal projects and learning.{" "}
|
||||||
|
<strong>If you’re using Yaak at work, a license is required.</strong>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Licenses help keep Yaak independent and sustainable.{" "}
|
||||||
|
<Link href="https://yaak.app/pricing?s=badge">Purchase a License →</Link>
|
||||||
|
</p>
|
||||||
|
</VStack>
|
||||||
|
),
|
||||||
|
requireTyping: "Personal Use",
|
||||||
|
color: "info",
|
||||||
|
});
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await patchModel(settings, { hideLicenseBadge });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingsSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
-3
@@ -1,18 +1,16 @@
|
|||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||||
import { useLicense } from "@yaakapp-internal/license";
|
import { useLicense } from "@yaakapp-internal/license";
|
||||||
|
import { Banner, HStack, Icon, VStack } from "@yaakapp-internal/ui";
|
||||||
import { differenceInDays } from "date-fns";
|
import { differenceInDays } from "date-fns";
|
||||||
import { formatDate } from "date-fns/format";
|
import { formatDate } from "date-fns/format";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useToggle } from "../../hooks/useToggle";
|
import { useToggle } from "../../hooks/useToggle";
|
||||||
import { pluralizeCount } from "../../lib/pluralize";
|
import { pluralizeCount } from "../../lib/pluralize";
|
||||||
import { CargoFeature } from "../CargoFeature";
|
import { CargoFeature } from "../CargoFeature";
|
||||||
import { Banner } from "../core/Banner";
|
|
||||||
import { Button } from "../core/Button";
|
import { Button } from "../core/Button";
|
||||||
import { Icon } from "../core/Icon";
|
|
||||||
import { Link } from "../core/Link";
|
import { Link } from "../core/Link";
|
||||||
import { PlainInput } from "../core/PlainInput";
|
import { PlainInput } from "../core/PlainInput";
|
||||||
import { Separator } from "../core/Separator";
|
import { Separator } from "../core/Separator";
|
||||||
import { HStack, VStack } from "../core/Stacks";
|
|
||||||
|
|
||||||
export function SettingsLicense() {
|
export function SettingsLicense() {
|
||||||
return (
|
return (
|
||||||
+13
-6
@@ -9,10 +9,22 @@ import {
|
|||||||
searchPlugins,
|
searchPlugins,
|
||||||
uninstallPlugin,
|
uninstallPlugin,
|
||||||
} from "@yaakapp-internal/plugins";
|
} from "@yaakapp-internal/plugins";
|
||||||
|
import {
|
||||||
|
HStack,
|
||||||
|
Icon,
|
||||||
|
InlineCode,
|
||||||
|
LoadingIcon,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableRow,
|
||||||
|
useDebouncedValue,
|
||||||
|
} from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useDebouncedValue } from "../../hooks/useDebouncedValue";
|
|
||||||
import { useInstallPlugin } from "../../hooks/useInstallPlugin";
|
import { useInstallPlugin } from "../../hooks/useInstallPlugin";
|
||||||
import { usePluginInfo } from "../../hooks/usePluginInfo";
|
import { usePluginInfo } from "../../hooks/usePluginInfo";
|
||||||
import { usePluginsKey, useRefreshPlugins } from "../../hooks/usePlugins";
|
import { usePluginsKey, useRefreshPlugins } from "../../hooks/usePlugins";
|
||||||
@@ -21,14 +33,9 @@ import { minPromiseMillis } from "../../lib/minPromiseMillis";
|
|||||||
import { Button } from "../core/Button";
|
import { Button } from "../core/Button";
|
||||||
import { Checkbox } from "../core/Checkbox";
|
import { Checkbox } from "../core/Checkbox";
|
||||||
import { CountBadge } from "../core/CountBadge";
|
import { CountBadge } from "../core/CountBadge";
|
||||||
import { Icon } from "../core/Icon";
|
|
||||||
import { IconButton } from "../core/IconButton";
|
import { IconButton } from "../core/IconButton";
|
||||||
import { InlineCode } from "../core/InlineCode";
|
|
||||||
import { Link } from "../core/Link";
|
import { Link } from "../core/Link";
|
||||||
import { LoadingIcon } from "../core/LoadingIcon";
|
|
||||||
import { PlainInput } from "../core/PlainInput";
|
import { PlainInput } from "../core/PlainInput";
|
||||||
import { HStack } from "../core/Stacks";
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "../core/Table";
|
|
||||||
import { TabContent, Tabs } from "../core/Tabs/Tabs";
|
import { TabContent, Tabs } from "../core/Tabs/Tabs";
|
||||||
import { EmptyStateText } from "../EmptyStateText";
|
import { EmptyStateText } from "../EmptyStateText";
|
||||||
import { SelectFile } from "../SelectFile";
|
import { SelectFile } from "../SelectFile";
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
|
||||||
|
import type { ProxySetting } from "@yaakapp-internal/models";
|
||||||
|
import { Heading, InlineCode, VStack } from "@yaakapp-internal/ui";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import {
|
||||||
|
SettingRowBoolean,
|
||||||
|
SettingRowSelect,
|
||||||
|
SettingRowText,
|
||||||
|
SettingsList,
|
||||||
|
SettingsSection,
|
||||||
|
} from "../core/SettingRow";
|
||||||
|
|
||||||
|
export function SettingsProxy() {
|
||||||
|
const settings = useAtomValue(settingsAtom);
|
||||||
|
const proxy = enabledProxyOrDefault(settings.proxy);
|
||||||
|
|
||||||
|
const patchProxy = async (patch: Partial<EnabledProxySetting>) => {
|
||||||
|
await patchModel(settings, {
|
||||||
|
proxy: {
|
||||||
|
...proxy,
|
||||||
|
...patch,
|
||||||
|
auth: Object.hasOwn(patch, "auth") ? (patch.auth ?? null) : proxy.auth,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack space={1.5} className="mb-4">
|
||||||
|
<div className="mb-3">
|
||||||
|
<Heading>Proxy</Heading>
|
||||||
|
<p className="text-text-subtle">
|
||||||
|
Configure a proxy server for HTTP requests. Useful for corporate firewalls, debugging
|
||||||
|
traffic, or routing through specific infrastructure.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<SettingsList className="space-y-8">
|
||||||
|
<SettingsSection title="Proxy">
|
||||||
|
<SettingRowSelect
|
||||||
|
title="Proxy"
|
||||||
|
description="Choose how Yaak should discover or use proxy settings."
|
||||||
|
name="proxy"
|
||||||
|
value={settings.proxy?.type ?? "automatic"}
|
||||||
|
onChange={async (v) => {
|
||||||
|
if (v === "automatic") {
|
||||||
|
await patchModel(settings, { proxy: undefined });
|
||||||
|
} else if (v === "enabled") {
|
||||||
|
await patchModel(settings, { proxy });
|
||||||
|
} else {
|
||||||
|
await patchModel(settings, { proxy: { type: "disabled" } });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={[
|
||||||
|
{ label: "Automatic proxy detection", value: "automatic" },
|
||||||
|
{ label: "Custom proxy configuration", value: "enabled" },
|
||||||
|
{ label: "No proxy", value: "disabled" },
|
||||||
|
]}
|
||||||
|
selectClassName="!w-64"
|
||||||
|
/>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
|
{settings.proxy?.type === "enabled" && (
|
||||||
|
<>
|
||||||
|
<SettingsSection title="Custom Proxy">
|
||||||
|
<SettingRowBoolean
|
||||||
|
checked={!settings.proxy.disabled}
|
||||||
|
title="Enable proxy"
|
||||||
|
description="Temporarily disable the proxy without losing the configuration."
|
||||||
|
onChange={(enabled) => patchProxy({ disabled: !enabled })}
|
||||||
|
/>
|
||||||
|
<SettingRowText
|
||||||
|
name="proxyHttp"
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Proxy for <InlineCode>http://</InlineCode> traffic
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
description="Proxy host used for unencrypted HTTP traffic."
|
||||||
|
value={settings.proxy.http}
|
||||||
|
placeholder="localhost:9090"
|
||||||
|
onChange={(http) => patchProxy({ http })}
|
||||||
|
/>
|
||||||
|
<SettingRowText
|
||||||
|
name="proxyHttps"
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Proxy for <InlineCode>https://</InlineCode> traffic
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
description="Proxy host used for HTTPS traffic."
|
||||||
|
value={settings.proxy.https}
|
||||||
|
placeholder="localhost:9090"
|
||||||
|
onChange={(https) => patchProxy({ https })}
|
||||||
|
/>
|
||||||
|
<SettingRowText
|
||||||
|
name="proxyBypass"
|
||||||
|
title="Proxy Bypass"
|
||||||
|
description="Comma-separated list of hosts that should bypass the proxy."
|
||||||
|
value={settings.proxy.bypass}
|
||||||
|
placeholder="127.0.0.1, *.example.com, localhost:3000"
|
||||||
|
inputWidthClassName="!w-96"
|
||||||
|
onChange={(bypass) => patchProxy({ bypass })}
|
||||||
|
/>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
|
<SettingsSection title="Authentication">
|
||||||
|
<SettingRowBoolean
|
||||||
|
checked={settings.proxy.auth != null}
|
||||||
|
title="Enable authentication"
|
||||||
|
description="Send proxy credentials with proxied requests."
|
||||||
|
onChange={(enabled) =>
|
||||||
|
patchProxy({ auth: enabled ? { user: "", password: "" } : null })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{settings.proxy.auth != null && (
|
||||||
|
<>
|
||||||
|
<SettingRowText
|
||||||
|
required
|
||||||
|
name="proxyUser"
|
||||||
|
title="User"
|
||||||
|
description="Username for proxy authentication."
|
||||||
|
value={settings.proxy.auth.user}
|
||||||
|
placeholder="myUser"
|
||||||
|
onChange={(user) =>
|
||||||
|
patchProxy({
|
||||||
|
auth: {
|
||||||
|
user,
|
||||||
|
password:
|
||||||
|
settings.proxy?.type === "enabled"
|
||||||
|
? (settings.proxy.auth?.password ?? "")
|
||||||
|
: "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SettingRowText
|
||||||
|
name="proxyPassword"
|
||||||
|
title="Password"
|
||||||
|
description="Password for proxy authentication."
|
||||||
|
value={settings.proxy.auth.password}
|
||||||
|
placeholder="s3cretPassw0rd"
|
||||||
|
type="password"
|
||||||
|
onChange={(password) =>
|
||||||
|
patchProxy({
|
||||||
|
auth: {
|
||||||
|
user:
|
||||||
|
settings.proxy?.type === "enabled"
|
||||||
|
? (settings.proxy.auth?.user ?? "")
|
||||||
|
: "",
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SettingsSection>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SettingsList>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type EnabledProxySetting = Extract<ProxySetting, { type: "enabled" }>;
|
||||||
|
|
||||||
|
function enabledProxyOrDefault(proxy: ProxySetting | null): EnabledProxySetting {
|
||||||
|
if (proxy?.type === "enabled") return proxy;
|
||||||
|
|
||||||
|
return {
|
||||||
|
disabled: false,
|
||||||
|
type: "enabled",
|
||||||
|
http: "",
|
||||||
|
https: "",
|
||||||
|
auth: { user: "", password: "" },
|
||||||
|
bypass: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
|
||||||
|
import { Heading, HStack, Icon, type IconProps, VStack } from "@yaakapp-internal/ui";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import { lazy, Suspense } from "react";
|
||||||
|
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
|
||||||
|
import { useResolvedAppearance } from "../../hooks/useResolvedAppearance";
|
||||||
|
import { useResolvedTheme } from "../../hooks/useResolvedTheme";
|
||||||
|
import type { ButtonProps } from "../core/Button";
|
||||||
|
import { IconButton } from "../core/IconButton";
|
||||||
|
import { Link } from "../core/Link";
|
||||||
|
import type { SelectProps } from "../core/Select";
|
||||||
|
import {
|
||||||
|
ModelSettingRowSelect,
|
||||||
|
SettingRowSelect,
|
||||||
|
SettingsList,
|
||||||
|
SettingsSection,
|
||||||
|
} from "../core/SettingRow";
|
||||||
|
|
||||||
|
const Editor = lazy(() => import("../core/Editor/Editor").then((m) => ({ default: m.Editor })));
|
||||||
|
|
||||||
|
const buttonColors: ButtonProps["color"][] = [
|
||||||
|
"primary",
|
||||||
|
"info",
|
||||||
|
"success",
|
||||||
|
"notice",
|
||||||
|
"warning",
|
||||||
|
"danger",
|
||||||
|
"secondary",
|
||||||
|
"default",
|
||||||
|
];
|
||||||
|
|
||||||
|
const icons: IconProps["icon"][] = [
|
||||||
|
"info",
|
||||||
|
"box",
|
||||||
|
"update",
|
||||||
|
"alert_triangle",
|
||||||
|
"arrow_big_right_dash",
|
||||||
|
"download",
|
||||||
|
"copy",
|
||||||
|
"magic_wand",
|
||||||
|
"settings",
|
||||||
|
"trash",
|
||||||
|
"sparkles",
|
||||||
|
"pencil",
|
||||||
|
"paste",
|
||||||
|
"search",
|
||||||
|
"send_horizontal",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function SettingsTheme() {
|
||||||
|
const workspace = useAtomValue(activeWorkspaceAtom);
|
||||||
|
const settings = useAtomValue(settingsAtom);
|
||||||
|
const appearance = useResolvedAppearance();
|
||||||
|
const activeTheme = useResolvedTheme();
|
||||||
|
|
||||||
|
if (settings == null || workspace == null || activeTheme.data == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lightThemes: SelectProps<string>["options"] = activeTheme.data.themes
|
||||||
|
.filter((theme) => !theme.dark)
|
||||||
|
.map((theme) => ({
|
||||||
|
label: theme.label,
|
||||||
|
value: theme.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const darkThemes: SelectProps<string>["options"] = activeTheme.data.themes
|
||||||
|
.filter((theme) => theme.dark)
|
||||||
|
.map((theme) => ({
|
||||||
|
label: theme.label,
|
||||||
|
value: theme.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack space={1.5} className="mb-4">
|
||||||
|
<div className="mb-3">
|
||||||
|
<Heading>Theme</Heading>
|
||||||
|
<p className="text-text-subtle">
|
||||||
|
Make Yaak your own by selecting a theme, or{" "}
|
||||||
|
<Link href="https://yaak.app/docs/plugin-development/plugins-quick-start">
|
||||||
|
Create Your Own
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<SettingsList className="space-y-8">
|
||||||
|
<SettingsSection title="Theme">
|
||||||
|
<ModelSettingRowSelect
|
||||||
|
model={settings}
|
||||||
|
modelKey="appearance"
|
||||||
|
title="Appearance"
|
||||||
|
description="Choose whether Yaak follows your system appearance or uses a fixed mode."
|
||||||
|
options={[
|
||||||
|
{ label: "Automatic", value: "system" },
|
||||||
|
{ label: "Light", value: "light" },
|
||||||
|
{ label: "Dark", value: "dark" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{(settings.appearance === "system" || settings.appearance === "light") && (
|
||||||
|
<SettingRowSelect
|
||||||
|
name="lightTheme"
|
||||||
|
title="Light theme"
|
||||||
|
description="Theme used when Yaak is in light mode."
|
||||||
|
value={activeTheme.data.light.id}
|
||||||
|
options={lightThemes}
|
||||||
|
onChange={(themeLight) => patchModel(settings, { themeLight })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(settings.appearance === "system" || settings.appearance === "dark") && (
|
||||||
|
<SettingRowSelect
|
||||||
|
name="darkTheme"
|
||||||
|
title="Dark theme"
|
||||||
|
description="Theme used when Yaak is in dark mode."
|
||||||
|
value={activeTheme.data.dark.id}
|
||||||
|
options={darkThemes}
|
||||||
|
onChange={(themeDark) => patchModel(settings, { themeDark })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
|
<SettingsSection title="Preview">
|
||||||
|
<VStack
|
||||||
|
space={3}
|
||||||
|
className="mt-4 w-full bg-surface p-3 border border-dashed border-border-subtle rounded overflow-x-auto"
|
||||||
|
>
|
||||||
|
<HStack className="text" space={1.5}>
|
||||||
|
<Icon icon={appearance === "dark" ? "moon" : "sun"} />
|
||||||
|
<strong>{activeTheme.data.active.label}</strong>
|
||||||
|
<em>(preview)</em>
|
||||||
|
</HStack>
|
||||||
|
<HStack space={1.5} className="w-full">
|
||||||
|
{buttonColors.map((c, i) => (
|
||||||
|
<IconButton
|
||||||
|
key={c}
|
||||||
|
color={c}
|
||||||
|
size="2xs"
|
||||||
|
iconSize="xs"
|
||||||
|
icon={icons[i % icons.length] ?? "info"}
|
||||||
|
iconClassName="text"
|
||||||
|
title={`${c}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{buttonColors.map((c, i) => (
|
||||||
|
<IconButton
|
||||||
|
key={c}
|
||||||
|
color={c}
|
||||||
|
variant="border"
|
||||||
|
size="2xs"
|
||||||
|
iconSize="xs"
|
||||||
|
icon={icons[i % icons.length] ?? "info"}
|
||||||
|
iconClassName="text"
|
||||||
|
title={`${c}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
<Suspense>
|
||||||
|
<Editor
|
||||||
|
defaultValue={[
|
||||||
|
"let foo = { // Demo code editor",
|
||||||
|
' foo: ("bar" || "baz" ?? \'qux\'),',
|
||||||
|
" baz: [1, 10.2, null, false, true],",
|
||||||
|
"};",
|
||||||
|
].join("\n")}
|
||||||
|
heightMode="auto"
|
||||||
|
language="javascript"
|
||||||
|
stateKey={null}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</VStack>
|
||||||
|
</SettingsSection>
|
||||||
|
</SettingsList>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user