mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-23 11:05:01 +01:00
Compare commits
7 Commits
v2025.5.0-
...
v2025.5.0-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6979cf37e | ||
|
|
ff26cc1344 | ||
|
|
fa62f88fa4 | ||
|
|
99975c3223 | ||
|
|
d3cda19be2 | ||
|
|
9b0a767ac8 | ||
|
|
81c3de807d |
@@ -24,5 +24,5 @@ the [Quick Start Guide](https://feedback.yaak.app/help/articles/6911763-plugins-
|
||||
If you prefer starting from scratch, manually install the types package:
|
||||
|
||||
```shell
|
||||
npm install @yaakapp/api
|
||||
npm install -D @yaakapp/api
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@yaakapp/api",
|
||||
"version": "0.6.4",
|
||||
"version": "0.6.5",
|
||||
"keywords": [
|
||||
"api-client",
|
||||
"insomnia-alternative",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
|
||||
import { JSONPath } from 'jsonpath-plus';
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
@@ -7,7 +7,13 @@ export const plugin: PluginDefinition = {
|
||||
name: 'json.jsonpath',
|
||||
description: 'Filter JSON-formatted text using JSONPath syntax',
|
||||
args: [
|
||||
{ type: 'text', name: 'input', label: 'Input', multiLine: true, placeholder: '{ "foo": "bar" }' },
|
||||
{
|
||||
type: 'text',
|
||||
name: 'input',
|
||||
label: 'Input',
|
||||
multiLine: true,
|
||||
placeholder: '{ "foo": "bar" }',
|
||||
},
|
||||
{ type: 'text', name: 'query', label: 'Query', placeholder: '$..foo' },
|
||||
{ type: 'checkbox', name: 'formatted', label: 'Format Output' },
|
||||
],
|
||||
@@ -28,7 +34,7 @@ export const plugin: PluginDefinition = {
|
||||
} else {
|
||||
return JSON.stringify(filtered);
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
@@ -37,12 +43,39 @@ export const plugin: PluginDefinition = {
|
||||
name: 'json.escape',
|
||||
description: 'Escape a JSON string, useful when using the output in JSON values',
|
||||
args: [
|
||||
{ type: 'text', name: 'input', label: 'Input', multiLine: true, placeholder: 'Hello "World"' },
|
||||
{
|
||||
type: 'text',
|
||||
name: 'input',
|
||||
label: 'Input',
|
||||
multiLine: true,
|
||||
placeholder: 'Hello "World"',
|
||||
},
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
const input = String(args.values.input ?? '');
|
||||
return input.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'json.minify',
|
||||
description: 'Remove unnecessary whitespace from a valid JSON string.',
|
||||
args: [
|
||||
{
|
||||
type: 'editor',
|
||||
language: 'json',
|
||||
name: 'input',
|
||||
label: 'Input',
|
||||
placeholder: '{ "foo": "bar" }',
|
||||
},
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
const input = String(args.values.input ?? '');
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(input));
|
||||
} catch {
|
||||
return input;
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -265,7 +265,7 @@ pub fn git_status(dir: &Path) -> Result<GitStatusSummary> {
|
||||
None => None,
|
||||
Some(t) => match t.get_path(&Path::new(&rela_path)) {
|
||||
Ok(entry) => {
|
||||
let obj = entry.to_object(&repo).unwrap();
|
||||
let obj = entry.to_object(&repo)?;
|
||||
let content = obj.as_blob().unwrap().content();
|
||||
let name = Path::new(entry.name().unwrap_or_default());
|
||||
SyncModel::from_bytes(content.into(), name)?.map(|m| m.0)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::connection_or_tx::ConnectionOrTx;
|
||||
use crate::error::Error::DBRowNotFound;
|
||||
use crate::error::Error::ModelNotFound;
|
||||
use crate::error::Result;
|
||||
use crate::models::{AnyModel, UpsertModelInfo};
|
||||
use crate::util::{ModelChangeEvent, ModelPayload, UpdateSource};
|
||||
use log::error;
|
||||
@@ -9,6 +10,7 @@ use sea_query::{
|
||||
SqliteQueryBuilder,
|
||||
};
|
||||
use sea_query_rusqlite::RusqliteBinder;
|
||||
use std::fmt::Debug;
|
||||
use std::sync::mpsc;
|
||||
|
||||
pub struct DbContext<'a> {
|
||||
@@ -19,15 +21,32 @@ pub struct DbContext<'a> {
|
||||
impl<'a> DbContext<'a> {
|
||||
pub(crate) fn find_one<'s, M>(
|
||||
&self,
|
||||
col: impl IntoColumnRef,
|
||||
value: impl Into<SimpleExpr>,
|
||||
) -> crate::error::Result<M>
|
||||
col: impl IntoColumnRef + IntoIden + Clone,
|
||||
value: impl Into<SimpleExpr> + Debug,
|
||||
) -> Result<M>
|
||||
where
|
||||
M: Into<AnyModel> + Clone + UpsertModelInfo,
|
||||
{
|
||||
match self.find_optional::<M>(col, value) {
|
||||
Some(v) => Ok(v),
|
||||
None => Err(DBRowNotFound(format!("{:?}", M::table_name()))),
|
||||
let value_debug = format!("{:?}", value);
|
||||
|
||||
let value_expr = value.into();
|
||||
let (sql, params) = Query::select()
|
||||
.from(M::table_name())
|
||||
.column(Asterisk)
|
||||
.cond_where(Expr::col(col.clone()).eq(value_expr))
|
||||
.build_rusqlite(SqliteQueryBuilder);
|
||||
let mut stmt = self.conn.prepare(sql.as_str()).expect("Failed to prepare query");
|
||||
match stmt.query_row(&*params.as_params(), M::from_row) {
|
||||
Ok(result) => Ok(result),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => {
|
||||
Err(ModelNotFound(format!(
|
||||
r#"table "{}" {} == {}"#,
|
||||
M::table_name().into_iden().to_string(),
|
||||
col.into_iden().to_string(),
|
||||
value_debug
|
||||
)))
|
||||
}
|
||||
Err(e) => Err(crate::error::Error::SqlError(e)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ pub enum Error {
|
||||
|
||||
#[error("Model error: {0}")]
|
||||
GenericError(String),
|
||||
|
||||
|
||||
#[error("DB Migration Failed: {0}")]
|
||||
MigrationError(String),
|
||||
|
||||
@@ -30,9 +30,6 @@ pub enum Error {
|
||||
#[error("Multiple base environments for {0}. Delete duplicates before continuing.")]
|
||||
MultipleBaseEnvironments(String),
|
||||
|
||||
#[error("Database row not found: {0}")]
|
||||
DBRowNotFound(String),
|
||||
|
||||
#[error("unknown error")]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ pub struct Settings {
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for Settings {
|
||||
fn table_name() -> impl IntoTableRef + Debug {
|
||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||
SettingsIden::Table
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ pub struct Workspace {
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for Workspace {
|
||||
fn table_name() -> impl IntoTableRef + Debug {
|
||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||
WorkspaceIden::Table
|
||||
}
|
||||
|
||||
@@ -355,7 +355,7 @@ pub struct WorkspaceMeta {
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for WorkspaceMeta {
|
||||
fn table_name() -> impl IntoTableRef + Debug {
|
||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||
WorkspaceMetaIden::Table
|
||||
}
|
||||
|
||||
@@ -456,7 +456,7 @@ pub struct CookieJar {
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for CookieJar {
|
||||
fn table_name() -> impl IntoTableRef + Debug {
|
||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||
CookieJarIden::Table
|
||||
}
|
||||
|
||||
@@ -535,7 +535,7 @@ pub struct Environment {
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for Environment {
|
||||
fn table_name() -> impl IntoTableRef + Debug {
|
||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||
EnvironmentIden::Table
|
||||
}
|
||||
|
||||
@@ -655,7 +655,7 @@ pub struct Folder {
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for Folder {
|
||||
fn table_name() -> impl IntoTableRef + Debug {
|
||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||
FolderIden::Table
|
||||
}
|
||||
|
||||
@@ -786,7 +786,7 @@ pub struct HttpRequest {
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for HttpRequest {
|
||||
fn table_name() -> impl IntoTableRef + Debug {
|
||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||
HttpRequestIden::Table
|
||||
}
|
||||
|
||||
@@ -913,7 +913,7 @@ pub struct WebsocketConnection {
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for WebsocketConnection {
|
||||
fn table_name() -> impl IntoTableRef + Debug {
|
||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||
WebsocketConnectionIden::Table
|
||||
}
|
||||
|
||||
@@ -1027,7 +1027,7 @@ pub struct WebsocketRequest {
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for WebsocketRequest {
|
||||
fn table_name() -> impl IntoTableRef + Debug {
|
||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||
WebsocketRequestIden::Table
|
||||
}
|
||||
|
||||
@@ -1152,7 +1152,7 @@ pub struct WebsocketEvent {
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for WebsocketEvent {
|
||||
fn table_name() -> impl IntoTableRef + Debug {
|
||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||
WebsocketEventIden::Table
|
||||
}
|
||||
|
||||
@@ -1269,7 +1269,7 @@ pub struct HttpResponse {
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for HttpResponse {
|
||||
fn table_name() -> impl IntoTableRef + Debug {
|
||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||
HttpResponseIden::Table
|
||||
}
|
||||
|
||||
@@ -1377,7 +1377,7 @@ pub struct GraphQlIntrospection {
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for GraphQlIntrospection {
|
||||
fn table_name() -> impl IntoTableRef + Debug {
|
||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||
GraphQlIntrospectionIden::Table
|
||||
}
|
||||
|
||||
@@ -1461,7 +1461,7 @@ pub struct GrpcRequest {
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for GrpcRequest {
|
||||
fn table_name() -> impl IntoTableRef + Debug {
|
||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||
GrpcRequestIden::Table
|
||||
}
|
||||
|
||||
@@ -1588,7 +1588,7 @@ pub struct GrpcConnection {
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for GrpcConnection {
|
||||
fn table_name() -> impl IntoTableRef + Debug {
|
||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||
GrpcConnectionIden::Table
|
||||
}
|
||||
|
||||
@@ -1708,7 +1708,7 @@ pub struct GrpcEvent {
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for GrpcEvent {
|
||||
fn table_name() -> impl IntoTableRef + Debug {
|
||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||
GrpcEventIden::Table
|
||||
}
|
||||
|
||||
@@ -1799,7 +1799,7 @@ pub struct Plugin {
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for Plugin {
|
||||
fn table_name() -> impl IntoTableRef + Debug {
|
||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||
PluginIden::Table
|
||||
}
|
||||
|
||||
@@ -1881,7 +1881,7 @@ pub struct SyncState {
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for SyncState {
|
||||
fn table_name() -> impl IntoTableRef + Debug {
|
||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||
SyncStateIden::Table
|
||||
}
|
||||
|
||||
@@ -1964,7 +1964,7 @@ pub struct KeyValue {
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for KeyValue {
|
||||
fn table_name() -> impl IntoTableRef + Debug {
|
||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||
KeyValueIden::Table
|
||||
}
|
||||
|
||||
@@ -2181,7 +2181,7 @@ impl AnyModel {
|
||||
}
|
||||
|
||||
pub trait UpsertModelInfo {
|
||||
fn table_name() -> impl IntoTableRef + Debug;
|
||||
fn table_name() -> impl IntoTableRef + IntoIden;
|
||||
fn id_column() -> impl IntoIden + Eq + Clone;
|
||||
fn generate_id() -> String;
|
||||
fn order_by() -> (impl IntoColumnRef, Order);
|
||||
|
||||
76
src-web/components/ConfirmLargeRequestBody.tsx
Normal file
76
src-web/components/ConfirmLargeRequestBody.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
import { patchModel } from '@yaakapp-internal/models';
|
||||
import { type ReactNode } from 'react';
|
||||
import { useToggle } from '../hooks/useToggle';
|
||||
import { showConfirm } from '../lib/confirm';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { Link } from './core/Link';
|
||||
import { SizeTag } from './core/SizeTag';
|
||||
import { HStack } from './core/Stacks';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
request: HttpRequest;
|
||||
}
|
||||
|
||||
const LARGE_TEXT_BYTES = 2 * 1000 * 1000;
|
||||
|
||||
export function ConfirmLargeRequestBody({ children, request }: Props) {
|
||||
const [showLargeResponse, toggleShowLargeResponse] = useToggle();
|
||||
|
||||
if (request.body?.text == null) {
|
||||
return children;
|
||||
}
|
||||
|
||||
const contentLength = request.body.text.length ?? 0;
|
||||
const tooLargeBytes = LARGE_TEXT_BYTES;
|
||||
const isLarge = contentLength > tooLargeBytes;
|
||||
if (!showLargeResponse && isLarge) {
|
||||
return (
|
||||
<Banner color="primary" className="flex flex-col gap-3">
|
||||
<p>
|
||||
Rendering content over{' '}
|
||||
<InlineCode>
|
||||
<SizeTag contentLength={tooLargeBytes} />
|
||||
</InlineCode>{' '}
|
||||
may impact performance.
|
||||
</p>
|
||||
<p>
|
||||
See{' '}
|
||||
<Link href="https://feedback.yaak.app/en/help/articles/1198684-working-with-large-values">
|
||||
Working With Large Values
|
||||
</Link>{' '}
|
||||
for tips.
|
||||
</p>
|
||||
<HStack wrap space={2}>
|
||||
<Button color="primary" size="xs" onClick={toggleShowLargeResponse}>
|
||||
Reveal Body
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
size="xs"
|
||||
variant="border"
|
||||
onClick={async () => {
|
||||
const confirm = await showConfirm({
|
||||
id: 'delete-body-' + request.id,
|
||||
confirmText: 'Delete Body',
|
||||
title: 'Delete Body Text',
|
||||
description: 'Are you sure you want to delete the request body text?',
|
||||
color: 'danger',
|
||||
});
|
||||
if (confirm) {
|
||||
await patchModel(request, { body: { ...request.body, text: '' } });
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete Body
|
||||
</Button>
|
||||
</HStack>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import { prepareImportQuerystring } from '../lib/prepareImportQuerystring';
|
||||
import { resolvedModelName } from '../lib/resolvedModelName';
|
||||
import { showToast } from '../lib/toast';
|
||||
import { BinaryFileEditor } from './BinaryFileEditor';
|
||||
import { ConfirmLargeRequestBody } from './ConfirmLargeRequestBody';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
import { Editor } from './core/Editor/Editor';
|
||||
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
|
||||
@@ -373,72 +374,74 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_BODY}>
|
||||
{activeRequest.bodyType === BODY_TYPE_JSON ? (
|
||||
<Editor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
placeholder="..."
|
||||
heightMode={fullHeight ? 'full' : 'auto'}
|
||||
defaultValue={`${activeRequest.body?.text ?? ''}`}
|
||||
language="json"
|
||||
onChange={handleBodyTextChange}
|
||||
stateKey={`json.${activeRequest.id}`}
|
||||
/>
|
||||
) : activeRequest.bodyType === BODY_TYPE_XML ? (
|
||||
<Editor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
placeholder="..."
|
||||
heightMode={fullHeight ? 'full' : 'auto'}
|
||||
defaultValue={`${activeRequest.body?.text ?? ''}`}
|
||||
language="xml"
|
||||
onChange={handleBodyTextChange}
|
||||
stateKey={`xml.${activeRequest.id}`}
|
||||
/>
|
||||
) : activeRequest.bodyType === BODY_TYPE_GRAPHQL ? (
|
||||
<GraphQLEditor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
baseRequest={activeRequest}
|
||||
request={activeRequest}
|
||||
onChange={handleBodyChange}
|
||||
/>
|
||||
) : activeRequest.bodyType === BODY_TYPE_FORM_URLENCODED ? (
|
||||
<FormUrlencodedEditor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
request={activeRequest}
|
||||
onChange={handleBodyChange}
|
||||
/>
|
||||
) : activeRequest.bodyType === BODY_TYPE_FORM_MULTIPART ? (
|
||||
<FormMultipartEditor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
request={activeRequest}
|
||||
onChange={handleBodyChange}
|
||||
/>
|
||||
) : activeRequest.bodyType === BODY_TYPE_BINARY ? (
|
||||
<BinaryFileEditor
|
||||
requestId={activeRequest.id}
|
||||
contentType={contentType}
|
||||
body={activeRequest.body}
|
||||
onChange={(body) => patchModel(activeRequest, { body })}
|
||||
onChangeContentType={handleContentTypeChange}
|
||||
/>
|
||||
) : typeof activeRequest.bodyType === 'string' ? (
|
||||
<Editor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
language={languageFromContentType(contentType)}
|
||||
placeholder="..."
|
||||
heightMode={fullHeight ? 'full' : 'auto'}
|
||||
defaultValue={`${activeRequest.body?.text ?? ''}`}
|
||||
onChange={handleBodyTextChange}
|
||||
stateKey={`other.${activeRequest.id}`}
|
||||
/>
|
||||
) : (
|
||||
<EmptyStateText>No Body</EmptyStateText>
|
||||
)}
|
||||
<ConfirmLargeRequestBody request={activeRequest}>
|
||||
{activeRequest.bodyType === BODY_TYPE_JSON ? (
|
||||
<Editor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
placeholder="..."
|
||||
heightMode={fullHeight ? 'full' : 'auto'}
|
||||
defaultValue={`${activeRequest.body?.text ?? ''}`}
|
||||
language="json"
|
||||
onChange={handleBodyTextChange}
|
||||
stateKey={`json.${activeRequest.id}`}
|
||||
/>
|
||||
) : activeRequest.bodyType === BODY_TYPE_XML ? (
|
||||
<Editor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
placeholder="..."
|
||||
heightMode={fullHeight ? 'full' : 'auto'}
|
||||
defaultValue={`${activeRequest.body?.text ?? ''}`}
|
||||
language="xml"
|
||||
onChange={handleBodyTextChange}
|
||||
stateKey={`xml.${activeRequest.id}`}
|
||||
/>
|
||||
) : activeRequest.bodyType === BODY_TYPE_GRAPHQL ? (
|
||||
<GraphQLEditor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
baseRequest={activeRequest}
|
||||
request={activeRequest}
|
||||
onChange={handleBodyChange}
|
||||
/>
|
||||
) : activeRequest.bodyType === BODY_TYPE_FORM_URLENCODED ? (
|
||||
<FormUrlencodedEditor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
request={activeRequest}
|
||||
onChange={handleBodyChange}
|
||||
/>
|
||||
) : activeRequest.bodyType === BODY_TYPE_FORM_MULTIPART ? (
|
||||
<FormMultipartEditor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
request={activeRequest}
|
||||
onChange={handleBodyChange}
|
||||
/>
|
||||
) : activeRequest.bodyType === BODY_TYPE_BINARY ? (
|
||||
<BinaryFileEditor
|
||||
requestId={activeRequest.id}
|
||||
contentType={contentType}
|
||||
body={activeRequest.body}
|
||||
onChange={(body) => patchModel(activeRequest, { body })}
|
||||
onChangeContentType={handleContentTypeChange}
|
||||
/>
|
||||
) : typeof activeRequest.bodyType === 'string' ? (
|
||||
<Editor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
language={languageFromContentType(contentType)}
|
||||
placeholder="..."
|
||||
heightMode={fullHeight ? 'full' : 'auto'}
|
||||
defaultValue={`${activeRequest.body?.text ?? ''}`}
|
||||
onChange={handleBodyTextChange}
|
||||
stateKey={`other.${activeRequest.id}`}
|
||||
/>
|
||||
) : (
|
||||
<EmptyStateText>No Body</EmptyStateText>
|
||||
)}
|
||||
</ConfirmLargeRequestBody>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_DESCRIPTION}>
|
||||
<div className="grid grid-rows-[auto_minmax(0,1fr)] h-full">
|
||||
|
||||
@@ -47,7 +47,7 @@ export function Dialog({
|
||||
<div
|
||||
role="dialog"
|
||||
className={classNames(
|
||||
'x-theme-dialog absolute inset-0 pointer-events-none',
|
||||
'py-4 x-theme-dialog absolute inset-0 pointer-events-none',
|
||||
'h-full flex flex-col items-center justify-center',
|
||||
vAlign === 'top' && 'justify-start',
|
||||
vAlign === 'center' && 'justify-center',
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getAnyModel, patchModelById } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { useDrop } from 'react-dnd';
|
||||
import { useKey, useKeyPressEvent } from 'react-use';
|
||||
import { activeRequestIdAtom } from '../../hooks/useActiveRequestId';
|
||||
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
|
||||
@@ -22,6 +23,8 @@ import { router } from '../../lib/router';
|
||||
import { setWorkspaceSearchParams } from '../../lib/setWorkspaceSearchParams';
|
||||
import { ContextMenu } from '../core/Dropdown';
|
||||
import { GitDropdown } from '../GitDropdown';
|
||||
import type { DragItem } from './dnd';
|
||||
import { ItemTypes } from './dnd';
|
||||
import { sidebarSelectedIdAtom, sidebarTreeAtom } from './SidebarAtoms';
|
||||
import type { SidebarItemProps } from './SidebarItem';
|
||||
import { SidebarItems } from './SidebarItems';
|
||||
@@ -204,15 +207,22 @@ export function Sidebar({ className }: Props) {
|
||||
[hasFocus, selectableRequests, selectedId, setSelectedId, setSelectedTree],
|
||||
);
|
||||
|
||||
const handleMoveToSidebarEnd = useCallback(() => {
|
||||
setHoveredTree(tree);
|
||||
// Put at the end of the top tree
|
||||
setHoveredIndex(tree?.children?.length ?? 0);
|
||||
}, [tree]);
|
||||
|
||||
const handleMove = useCallback<SidebarItemProps['onMove']>(
|
||||
async (id, side) => {
|
||||
(id, side) => {
|
||||
let hoveredTree = treeParentMap[id] ?? null;
|
||||
const dragIndex = hoveredTree?.children.findIndex((n) => n.id === id) ?? -99;
|
||||
const hoveredItem = hoveredTree?.children[dragIndex] ?? null;
|
||||
let hoveredIndex = dragIndex + (side === 'above' ? 0 : 1);
|
||||
|
||||
const isHoveredItemCollapsed =
|
||||
hoveredItem != null ? getSidebarCollapsedMap()[hoveredItem.id] : false;
|
||||
const collapsedMap = getSidebarCollapsedMap();
|
||||
const isHoveredItemCollapsed = hoveredItem != null ? collapsedMap[hoveredItem.id] : false;
|
||||
|
||||
if (hoveredItem?.model === 'folder' && side === 'below' && !isHoveredItemCollapsed) {
|
||||
// Move into the folder if it's open and we're moving below it
|
||||
hoveredTree = hoveredTree?.children.find((n) => n.id === id) ?? null;
|
||||
@@ -232,6 +242,7 @@ export function Sidebar({ className }: Props) {
|
||||
const handleEnd = useCallback<SidebarItemProps['onEnd']>(
|
||||
async (itemId) => {
|
||||
setHoveredTree(null);
|
||||
setDraggingId(null);
|
||||
handleClearSelected();
|
||||
|
||||
if (hoveredTree == null || hoveredIndex == null) {
|
||||
@@ -278,9 +289,8 @@ export function Sidebar({ className }: Props) {
|
||||
);
|
||||
} else {
|
||||
const sortPriority = afterPriority - (afterPriority - beforePriority) / 2;
|
||||
return patchModelById(child.model, child.id, { sortPriority, folderId });
|
||||
await patchModelById(child.model, child.id, { sortPriority, folderId });
|
||||
}
|
||||
setDraggingId(null);
|
||||
},
|
||||
[handleClearSelected, hoveredTree, hoveredIndex, treeParentMap],
|
||||
);
|
||||
@@ -298,6 +308,20 @@ export function Sidebar({ className }: Props) {
|
||||
|
||||
const mainContextMenuItems = useCreateDropdownItems({ folderId: null });
|
||||
|
||||
const [, connectDrop] = useDrop<DragItem, void>(
|
||||
{
|
||||
accept: ItemTypes.REQUEST,
|
||||
hover: (_, monitor) => {
|
||||
if (sidebarRef.current == null) return;
|
||||
if (!monitor.isOver({ shallow: true })) return;
|
||||
handleMoveToSidebarEnd();
|
||||
},
|
||||
},
|
||||
[handleMoveToSidebarEnd],
|
||||
);
|
||||
|
||||
connectDrop(sidebarRef);
|
||||
|
||||
// Not ready to render yet
|
||||
if (tree == null) {
|
||||
return null;
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type WebsocketRequest,
|
||||
} from '@yaakapp-internal/models';
|
||||
|
||||
// This is an atom so we can use it in the child items to avoid re-rendering the entire list
|
||||
// This is an atom, so we can use it in the child items to avoid re-rendering the entire list
|
||||
import { atom } from 'jotai';
|
||||
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
|
||||
import { allRequestsAtom } from '../../hooks/useAllRequests';
|
||||
|
||||
@@ -20,15 +20,13 @@ import { HttpMethodTag } from '../core/HttpMethodTag';
|
||||
import { HttpStatusTag } from '../core/HttpStatusTag';
|
||||
import { Icon } from '../core/Icon';
|
||||
import { LoadingIcon } from '../core/LoadingIcon';
|
||||
import type { DragItem} from './dnd';
|
||||
import { ItemTypes } from './dnd';
|
||||
import type { SidebarTreeNode } from './Sidebar';
|
||||
import { sidebarSelectedIdAtom } from './SidebarAtoms';
|
||||
import { SidebarItemContextMenu } from './SidebarItemContextMenu';
|
||||
import type { SidebarItemsProps } from './SidebarItems';
|
||||
|
||||
enum ItemTypes {
|
||||
REQUEST = 'request',
|
||||
}
|
||||
|
||||
export type SidebarItemProps = {
|
||||
className?: string;
|
||||
itemId: string;
|
||||
@@ -44,11 +42,6 @@ export type SidebarItemProps = {
|
||||
latestWebsocketConnection: WebsocketConnection | null;
|
||||
} & Pick<SidebarItemsProps, 'onSelect'>;
|
||||
|
||||
type DragItem = {
|
||||
id: string;
|
||||
itemName: string;
|
||||
};
|
||||
|
||||
export const SidebarItem = memo(function SidebarItem({
|
||||
itemName,
|
||||
itemId,
|
||||
@@ -69,9 +62,10 @@ export const SidebarItem = memo(function SidebarItem({
|
||||
|
||||
const [, connectDrop] = useDrop<DragItem, void>(
|
||||
{
|
||||
accept: ItemTypes.REQUEST,
|
||||
accept: [ItemTypes.REQUEST, ItemTypes.SIDEBAR],
|
||||
hover: (_, monitor) => {
|
||||
if (!ref.current) return;
|
||||
if (!monitor.isOver()) return;
|
||||
const hoverBoundingRect = ref.current?.getBoundingClientRect();
|
||||
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
|
||||
9
src-web/components/sidebar/dnd.ts
Normal file
9
src-web/components/sidebar/dnd.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export enum ItemTypes {
|
||||
REQUEST = 'request',
|
||||
SIDEBAR = 'sidebar',
|
||||
}
|
||||
|
||||
export type DragItem = {
|
||||
id: string;
|
||||
itemName: string;
|
||||
};
|
||||
@@ -8,7 +8,7 @@ export function useToggleCommandPalette() {
|
||||
id: 'command_palette',
|
||||
size: 'dynamic',
|
||||
hideX: true,
|
||||
className: '!max-h-[min(30rem,calc(100vh-4rem))]',
|
||||
className: 'mb-auto mt-[4rem] !max-h-[min(30rem,calc(100vh-4rem))]',
|
||||
vAlign: 'top',
|
||||
noPadding: true,
|
||||
noScroll: true,
|
||||
|
||||
Reference in New Issue
Block a user