Compare commits

..

7 Commits

Author SHA1 Message Date
Gregory Schier
a6979cf37e Print table/col/val when row not found 2025-07-02 08:14:52 -07:00
Gregory Schier
ff26cc1344 Tweaks 2025-07-02 07:47:36 -07:00
Gregory Schier
fa62f88fa4 Allow moving requests and folders to end of list 2025-06-29 08:40:14 -07:00
Gregory Schier
99975c3223 Fix sidebar folder dragging collapse
https://feedback.yaak.app/p/a-folder-may-hide-its-content-if-i-move-a
2025-06-29 08:02:55 -07:00
Gregory Schier
d3cda19be2 Hide large request bodies by default 2025-06-29 07:30:07 -07:00
Gregory Schier
9b0a767ac8 Prevent command palette from jumping with less results 2025-06-28 07:37:15 -07:00
Gregory Schier
81c3de807d Add json.minify 2025-06-28 07:29:24 -07:00
15 changed files with 276 additions and 121 deletions

View File

@@ -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
```

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp/api",
"version": "0.6.4",
"version": "0.6.5",
"keywords": [
"api-client",
"insomnia-alternative",

View File

@@ -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;
}
},
},
],
};

View File

@@ -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)

View File

@@ -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)),
}
}

View File

@@ -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,
}

View File

@@ -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);

View 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}</>;
}

View File

@@ -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">

View File

@@ -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',

View File

@@ -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;

View File

@@ -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';

View File

@@ -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();

View File

@@ -0,0 +1,9 @@
export enum ItemTypes {
REQUEST = 'request',
SIDEBAR = 'sidebar',
}
export type DragItem = {
id: string;
itemName: string;
};

View File

@@ -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,