From c4ab045e577d0bc45a3a74a9ab606a97268ab12e Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Sun, 19 Mar 2023 00:48:09 -0700 Subject: [PATCH] Persist sort priority! --- src-tauri/icons/icon.icns | Bin 616124 -> 616124 bytes .../20230319042610_sort-priority.sql | 1 + src-tauri/sqlx-data.json | 194 +++++---- src-tauri/src/main.rs | 4 +- src-tauri/src/models.rs | 13 +- src-web/components/Sidebar.tsx | 396 ++++++++++++------ src-web/components/UrlBar.tsx | 1 + src-web/components/Workspace.tsx | 10 +- src-web/components/WorkspaceDropdown.tsx | 9 +- src-web/components/core/Button.tsx | 2 +- src-web/components/core/Dropdown.tsx | 2 +- src-web/components/core/ScrollArea.tsx | 8 +- src-web/components/core/Stacks.tsx | 12 +- src-web/hooks/useCreateRequest.ts | 3 +- src-web/hooks/useUpdateAnyRequest.ts | 26 ++ src-web/lib/models.ts | 1 + src-web/main.css | 18 +- 17 files changed, 457 insertions(+), 243 deletions(-) create mode 100644 src-tauri/migrations/20230319042610_sort-priority.sql create mode 100644 src-web/hooks/useUpdateAnyRequest.ts diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns index 141f943c2ca3569c73ebe7a6713fd9cbcb1140d4..581fdf3c6bc66769da83b87a5966b38e21d666f7 100644 GIT binary patch delta 90 zcmdmUR&~!=RgTQ$ykZ8PXO6=QXlsG;u+n&kA?#sEI*-o4b9hF#A%z?K{@8_)4@#0aZsSaeP_UEXdL>$ifK3OhC-MU66%ke(pw2HWmPC CXd)c| diff --git a/src-tauri/migrations/20230319042610_sort-priority.sql b/src-tauri/migrations/20230319042610_sort-priority.sql new file mode 100644 index 00000000..29a19241 --- /dev/null +++ b/src-tauri/migrations/20230319042610_sort-priority.sql @@ -0,0 +1 @@ +ALTER TABLE main.http_requests ADD COLUMN sort_priority REAL NOT NULL DEFAULT 0; diff --git a/src-tauri/sqlx-data.json b/src-tauri/sqlx-data.json index 7b0bf4fc..a6586825 100644 --- a/src-tauri/sqlx-data.json +++ b/src-tauri/sqlx-data.json @@ -78,7 +78,7 @@ }, "query": "\n DELETE FROM http_requests\n WHERE id = ?\n " }, - "68b7b17a25d415ce90b33aef16418d668f7ff6275b383bf21c16cafb38cca342": { + "539bb11d635c0295f969d32c6bf1e3d78f2686521a5ef2a4af661b7e645f58c1": { "describe": { "columns": [ { @@ -132,8 +132,13 @@ "type_info": "Text" }, { - "name": "headers!: sqlx::types::Json>", + "name": "sort_priority", "ordinal": 10, + "type_info": "Float" + }, + { + "name": "headers!: sqlx::types::Json>", + "ordinal": 11, "type_info": "Text" } ], @@ -148,23 +153,14 @@ false, true, true, + false, false ], "parameters": { "Right": 1 } }, - "query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body,\n body_type,\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_requests\n WHERE workspace_id = ?\n " - }, - "913f3c3a46b1834c4cd8367aed9d5a9659a1d775d8771e9f5bf9a5aa41197356": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 8 - } - }, - "query": "\n INSERT INTO http_requests (\n id,\n workspace_id,\n name,\n url,\n method,\n body,\n body_type,\n headers\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n method = excluded.method,\n headers = excluded.headers,\n body = excluded.body,\n body_type = excluded.body_type,\n url = excluded.url\n " + "query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body,\n body_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_requests\n WHERE workspace_id = ?\n " }, "a83698dcf9a815b881097133edb31a34ba25e7c6c114d463c495342a85371639": { "describe": { @@ -372,84 +368,6 @@ }, "query": "\n INSERT INTO key_values (namespace, key, value)\n VALUES (?, ?, ?) ON CONFLICT DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n value = excluded.value\n " }, - "d9ea350bc21ac2f51f6dcb9713328ec330f0e12105da70bf5a6eff9601e32a85": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "model", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "workspace_id", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "created_at", - "ordinal": 3, - "type_info": "Datetime" - }, - { - "name": "updated_at", - "ordinal": 4, - "type_info": "Datetime" - }, - { - "name": "name", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "url", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "method", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "body", - "ordinal": 8, - "type_info": "Text" - }, - { - "name": "body_type", - "ordinal": 9, - "type_info": "Text" - }, - { - "name": "headers!: sqlx::types::Json>", - "ordinal": 10, - "type_info": "Text" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - true, - true, - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body,\n body_type,\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_requests\n WHERE id = ?\n " - }, "e3ade0a69348d512e47e964bded9d7d890b92fdc1e01c6c22fa5e91f943639f2": { "describe": { "columns": [ @@ -540,6 +458,90 @@ }, "query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at,\n status, status_reason, body, elapsed, url, error,\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_responses\n WHERE id = ?\n " }, + "e523dc91256b4409a734850eae59ac73b951177ce88d35e2ab708871f3067ace": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "model", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "workspace_id", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 3, + "type_info": "Datetime" + }, + { + "name": "updated_at", + "ordinal": 4, + "type_info": "Datetime" + }, + { + "name": "name", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "url", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "method", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "body", + "ordinal": 8, + "type_info": "Text" + }, + { + "name": "body_type", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "sort_priority", + "ordinal": 10, + "type_info": "Float" + }, + { + "name": "headers!: sqlx::types::Json>", + "ordinal": 11, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body,\n body_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_requests\n WHERE id = ?\n " + }, "e767522f92c8c49cd2e563e58737a05092daf9b1dc763bacc82a5c14d696d78e": { "describe": { "columns": [], @@ -559,5 +561,15 @@ } }, "query": "\n INSERT INTO workspaces (id, name, description)\n VALUES (?, ?, ?)\n " + }, + "f506d6b1451d95489cf41fea2d1cd3fae4f0773e16ae11ded6fd5923f015c8d5": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 9 + } + }, + "query": "\n INSERT INTO http_requests (\n id,\n workspace_id,\n name,\n url,\n method,\n body,\n body_type,\n headers,\n sort_priority\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n method = excluded.method,\n headers = excluded.headers,\n body = excluded.body,\n body_type = excluded.body_type,\n url = excluded.url,\n sort_priority = excluded.sort_priority\n " } } \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index c1d53298..a4a8069f 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -247,13 +247,14 @@ async fn create_workspace( async fn create_request( workspace_id: &str, name: &str, + sort_priority: f64, app_handle: AppHandle, db_instance: State<'_, Mutex>>, ) -> Result { let pool = &*db_instance.lock().await; let headers = Vec::new(); let created_request = - models::upsert_request(None, workspace_id, name, "GET", None, None, "", headers, pool) + models::upsert_request(None, workspace_id, name, "GET", None, None, "", headers, sort_priority, pool) .await .expect("Failed to create request"); @@ -291,6 +292,7 @@ async fn update_request( request.body_type, request.url.as_str(), request.headers.0, + request.sort_priority, pool, ) .await diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 4c5ed8bc..13bc9afe 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -29,6 +29,7 @@ pub struct HttpRequest { pub model: String, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, + pub sort_priority: f64, pub workspace_id: String, pub name: String, pub url: String, @@ -170,6 +171,7 @@ pub async fn upsert_request( body_type: Option, url: &str, headers: Vec, + sort_priority: f64, pool: &Pool, ) -> Result { let generated_id; @@ -191,9 +193,10 @@ pub async fn upsert_request( method, body, body_type, - headers + headers, + sort_priority ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET updated_at = CURRENT_TIMESTAMP, name = excluded.name, @@ -201,7 +204,8 @@ pub async fn upsert_request( headers = excluded.headers, body = excluded.body, body_type = excluded.body_type, - url = excluded.url + url = excluded.url, + sort_priority = excluded.sort_priority "#, id, workspace_id, @@ -211,6 +215,7 @@ pub async fn upsert_request( body, body_type, headers_json, + sort_priority, ) .execute(pool) .await @@ -236,6 +241,7 @@ pub async fn find_requests( method, body, body_type, + sort_priority, headers AS "headers!: sqlx::types::Json>" FROM http_requests WHERE workspace_id = ? @@ -261,6 +267,7 @@ pub async fn get_request(id: &str, pool: &Pool) -> Result>" FROM http_requests WHERE id = ? diff --git a/src-web/components/Sidebar.tsx b/src-web/components/Sidebar.tsx index 4423a6d2..168bde86 100644 --- a/src-web/components/Sidebar.tsx +++ b/src-web/components/Sidebar.tsx @@ -1,13 +1,20 @@ import classnames from 'classnames'; -import type { MouseEvent as ReactMouseEvent } from 'react'; -import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { DndProvider, useDrag, useDrop } from 'react-dnd'; +import type { + CSSProperties, + ForwardedRef, + KeyboardEvent, + MouseEvent as ReactMouseEvent, +} from 'react'; +import React, { forwardRef, Fragment, memo, useCallback, useMemo, useRef, useState } from 'react'; +import type { XYCoord } from 'react-dnd'; +import { DndProvider, useDrag, useDragLayer, useDrop } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { useActiveRequest } from '../hooks/useActiveRequest'; import { useCreateRequest } from '../hooks/useCreateRequest'; import { useDeleteRequest } from '../hooks/useDeleteRequest'; import { useKeyValue } from '../hooks/useKeyValue'; import { useRequests } from '../hooks/useRequests'; +import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest'; import { useUpdateRequest } from '../hooks/useUpdateRequest'; import { clamp } from '../lib/clamp'; import type { HttpRequest } from '../lib/models'; @@ -15,6 +22,7 @@ import { Button } from './core/Button'; import { Dropdown, DropdownMenuTrigger } from './core/Dropdown'; import { Icon } from './core/Icon'; import { IconButton } from './core/IconButton'; +import { ScrollArea } from './core/ScrollArea'; import { HStack, VStack } from './core/Stacks'; import { WindowDragRegion } from './core/WindowDragRegion'; import { ToggleThemeButton } from './ToggleThemeButton'; @@ -82,15 +90,7 @@ export function Container({ className }: Props) { const sidebarWidth = width.value - 1; // Minus 1 for the border return ( -
+
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
- - { - await createRequest.mutate({ name: 'Test Request' }); - }} - /> - - - - - - - +
+ + { + const lastRequest = requests[requests.length - 1]; + await createRequest.mutate({ + name: 'Test Request', + sortPriority: lastRequest?.sortPriority ?? 0 + 1, + }); + }} + /> + + + + + {/**/} + + + + + +
); } function SidebarItems({ - requests, + requests: unorderedRequests, activeRequestId, sidebarWidth, }: { @@ -138,44 +154,85 @@ function SidebarItems({ activeRequestId?: string; sidebarWidth: number; }) { - const [items, setItems] = useState(requests.map((r) => ({ request: r, left: 0, top: 0 }))); + const [hoveredIndex, setHoveredIndex] = useState(null); + const updateRequest = useUpdateAnyRequest(); + const requests = useMemo( + () => [...unorderedRequests].sort((a, b) => a.sortPriority - b.sortPriority), + [unorderedRequests], + ); - useEffect(() => { - setItems(requests.map((r) => ({ request: r, left: 0, top: 0 }))); - }, [requests]); + const handleMove = useCallback( + (id, side) => { + const dragIndex = requests.findIndex((r) => r.id === id); + setHoveredIndex(side === 'above' ? dragIndex : dragIndex + 1); + }, + [requests], + ); - const handleMove = useCallback((id: string, hoverId: string) => { - setItems((oldItems) => { - const dragIndex = oldItems.findIndex((i) => i.request.id === id); - const index = oldItems.findIndex((i) => i.request.id === hoverId); - const newItems = [...oldItems]; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const b = newItems[index]!; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - newItems[index] = newItems[dragIndex]!; - newItems[dragIndex] = b; - return newItems; - }); - }, []); + const handleCancel = useCallback(() => setHoveredIndex(null), []); + + const handleEnd = useCallback( + (requestId) => { + if (hoveredIndex === null) return; + setHoveredIndex(null); + + const index = requests.findIndex((r) => r.id === requestId); + const request = requests[index]; + if (request === undefined) return; + + const newRequests = requests.filter((r) => r.id !== requestId); + if (hoveredIndex > index) { + newRequests.splice(hoveredIndex - 1, 0, request); + } else { + newRequests.splice(hoveredIndex, 0, request); + } + + const beforePriority = newRequests[hoveredIndex - 1]?.sortPriority ?? 0; + const afterPriority = newRequests[hoveredIndex + 1]?.sortPriority ?? 0; + + const shouldUpdateAll = afterPriority - beforePriority < 1; + if (shouldUpdateAll) { + newRequests.forEach((r, i) => { + updateRequest.mutate({ id: r.id, sortPriority: i * 1000 }); + }); + } else { + updateRequest.mutate({ + id: requestId, + sortPriority: afterPriority - (afterPriority - beforePriority) / 2, + }); + } + }, + [hoveredIndex, requests], + ); return ( <> - {items.map(({ request }) => ( - - ))} + {requests.map((r, i) => { + return ( + + {hoveredIndex === i && } + + + ); + })} + {hoveredIndex === requests.length && } ); } type SidebarItemProps = { + className?: string; + buttonClassName?: string; requestId: string; requestName: string; workspaceId: string; @@ -183,13 +240,18 @@ type SidebarItemProps = { active?: boolean; }; -const SidebarItem = memo(function SidebarItem({ - requestName, - requestId, - workspaceId, - active, - sidebarWidth, -}: SidebarItemProps) { +const _SidebarItem = forwardRef(function SidebarItem( + { + className, + buttonClassName, + requestName, + requestId, + workspaceId, + active, + sidebarWidth, + }: SidebarItemProps, + ref: ForwardedRef, +) { const deleteRequest = useDeleteRequest(requestId); const updateRequest = useUpdateRequest(requestId); const [editing, setEditing] = useState(false); @@ -205,17 +267,60 @@ const SidebarItem = memo(function SidebarItem({ }, []); const itemStyles = useMemo(() => ({ width: sidebarWidth }), [sidebarWidth]); + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + // Hitting enter on active request during keyboard nav will start edit + if (active && e.key === 'Enter') { + e.preventDefault(); + setEditing(true); + } + }, + [active], + ); - if (workspaceId === null) return null; + const handleInputKeyDown = useCallback( + async (e: KeyboardEvent) => { + switch (e.key) { + case 'Enter': + await handleSubmitNameEdit(e.currentTarget); + break; + case 'Escape': + setEditing(false); + break; + } + }, + [active], + ); + + const actionItems = useMemo( + () => [ + { + label: 'Delete Request', + onSelect: deleteRequest.mutate, + leftSlot: , + }, + ], + [], + ); return ( -
  • +
  • - [ - { - label: 'Delete Request', - onSelect: deleteRequest.mutate, - leftSlot: , - }, - ], - [], - )} - > + ); }); +const SidebarItem = memo(_SidebarItem); type DraggableSidebarItemProps = SidebarItemProps & { - onMove: (id: string, hoverId: string) => void; + onMove: (id: string, side: 'above' | 'below') => void; + onEnd: (id: string) => void; + onCancel: () => void; }; type DragItem = { id: string; + workspaceId: string; + requestName: string; }; const DraggableSidebarItem = memo(function DraggableSidebarItem({ @@ -306,37 +385,108 @@ const DraggableSidebarItem = memo(function DraggableSidebarItem({ active, sidebarWidth, onMove, + onEnd, + onCancel, }: DraggableSidebarItemProps) { - const ref = useRef(null); + const ref = useRef(null); const [, connectDrop] = useDrop({ accept: ItemTypes.REQUEST, collect: (m) => ({ handlerId: m.getHandlerId(), isOver: m.isOver() }), - hover: (item) => { - if (item.id !== requestId) { - onMove(requestId, item.id); - } + hover: (item, monitor) => { + if (!ref.current) return; + const hoverBoundingRect = ref.current?.getBoundingClientRect(); + + const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; + const clientOffset = monitor.getClientOffset(); + const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top; + onMove(requestId, hoverClientY < hoverMiddleY ? 'above' : 'below'); }, }); - const [{ isDragging }, connectDrag] = useDrag(() => ({ - type: ItemTypes.REQUEST, - item: () => ({ id: requestId }), - collect: (m) => ({ isDragging: m.isDragging() }), - })); + const [{ isDragging }, connectDrag, preview] = useDrag< + DragItem, + unknown, + { isDragging: boolean } + >( + () => ({ + type: ItemTypes.REQUEST, + item: () => ({ id: requestId, requestName, workspaceId }), + collect: (m) => ({ isDragging: m.isDragging() }), + options: { dropEffect: 'move' }, + end: (item, monitor) => { + if (monitor.didDrop()) { + onEnd(requestId); + } else { + onCancel(); + } + }, + }), + [onEnd], + ); + + // preview(getEmptyImage(), { captureDraggingState: true }); connectDrag(ref); connectDrop(ref); return ( -
    - -
    + ); }); + +function CustomDragLayer({ sidebarWidth }: { sidebarWidth: number }) { + const { itemType, isDragging, item, currentOffset } = useDragLayer((monitor) => ({ + item: monitor.getItem(), + itemType: monitor.getItemType(), + currentOffset: monitor.getSourceClientOffset(), + isDragging: monitor.isDragging(), + })); + + const styles = useMemo(() => { + if (currentOffset === null) { + return { display: 'none' }; + } + const transform = `translate(${currentOffset.x}px, ${currentOffset.y}px)`; + return { transform, WebkitTransform: transform }; + }, [currentOffset]); + + if (!isDragging) { + return null; + } + + return ( +
    +
    + {itemType === ItemTypes.REQUEST && ( + + )} +
    +
    + ); +} + +const DropMarker = memo( + function DropMarker() { + return ( +
    +
    +
    + ); + }, + () => true, +); diff --git a/src-web/components/UrlBar.tsx b/src-web/components/UrlBar.tsx index 60567205..d353d5cc 100644 --- a/src-web/components/UrlBar.tsx +++ b/src-web/components/UrlBar.tsx @@ -26,6 +26,7 @@ export const UrlBar = memo(function UrlBar({ request }: Props) { className="w-full flex items-center" > 900; if (activeWorkspace == null) { @@ -40,7 +34,7 @@ export default function Workspace() { alignItems="center" >
    - +
    {activeRequest?.name} diff --git a/src-web/components/WorkspaceDropdown.tsx b/src-web/components/WorkspaceDropdown.tsx index 7887b186..36442f3b 100644 --- a/src-web/components/WorkspaceDropdown.tsx +++ b/src-web/components/WorkspaceDropdown.tsx @@ -1,3 +1,4 @@ +import classnames from 'classnames'; import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; @@ -8,7 +9,11 @@ import type { DropdownItem } from './core/Dropdown'; import { Dropdown, DropdownMenuTrigger } from './core/Dropdown'; import { Icon } from './core/Icon'; -export function WorkspaceDropdown() { +type Props = { + className?: string; +}; + +export function WorkspaceDropdown({ className }: Props) { const navigate = useNavigate(); const workspaces = useWorkspaces(); const activeWorkspace = useActiveWorkspace(); @@ -37,7 +42,7 @@ export function WorkspaceDropdown() { return ( - diff --git a/src-web/components/core/Button.tsx b/src-web/components/core/Button.tsx index d2b691d9..e02f82c3 100644 --- a/src-web/components/core/Button.tsx +++ b/src-web/components/core/Button.tsx @@ -43,7 +43,7 @@ const _Button = forwardRef(function Button( () => classnames( className, - 'outline-none pointer-events-auto', + 'outline-none', 'border border-transparent focus-visible:border-blue-300', 'rounded-md flex items-center', colorStyles[color || 'default'], diff --git a/src-web/components/core/Dropdown.tsx b/src-web/components/core/Dropdown.tsx index c340e90a..e997d197 100644 --- a/src-web/components/core/Dropdown.tsx +++ b/src-web/components/core/Dropdown.tsx @@ -143,7 +143,7 @@ const _DropdownMenuContent = forwardRef { const windowBox = document.documentElement.getBoundingClientRect(); const menuBox = divRef.getBoundingClientRect(); - const styles = { maxHeight: windowBox.height - menuBox.top - 5 - 45 }; + const styles = { maxHeight: windowBox.height - menuBox.top - 5 }; setStyles(styles); }); return () => clearTimeout(t); diff --git a/src-web/components/core/ScrollArea.tsx b/src-web/components/core/ScrollArea.tsx index 394e0883..46c8c812 100644 --- a/src-web/components/core/ScrollArea.tsx +++ b/src-web/components/core/ScrollArea.tsx @@ -9,8 +9,8 @@ interface Props { export function ScrollArea({ children, className }: Props) { return ( - - {children} + + {children} @@ -23,12 +23,12 @@ function ScrollBar({ orientation }: { orientation: 'vertical' | 'horizontal' }) - + ); } diff --git a/src-web/components/core/Stacks.tsx b/src-web/components/core/Stacks.tsx index 9974fa29..8cc1ed52 100644 --- a/src-web/components/core/Stacks.tsx +++ b/src-web/components/core/Stacks.tsx @@ -1,5 +1,5 @@ import classnames from 'classnames'; -import type { ComponentType, ReactNode } from 'react'; +import type { ComponentType, HTMLAttributes, ReactNode } from 'react'; const gapClasses = { 0: 'gap-0', @@ -23,9 +23,9 @@ export function HStack({ className, space, children, ...props }: HStackProps) { ); } -export interface VStackProps extends BaseStackProps { +export type VStackProps = BaseStackProps & { children: ReactNode; -} +}; export function VStack({ className, space, children, ...props }: VStackProps) { return ( @@ -38,14 +38,12 @@ export function VStack({ className, space, children, ...props }: VStackProps) { ); } -interface BaseStackProps { +type BaseStackProps = HTMLAttributes & { as?: ComponentType | 'ul'; space?: keyof typeof gapClasses; alignItems?: 'start' | 'center'; justifyContent?: 'start' | 'center' | 'end'; - className?: string; - children?: ReactNode; -} +}; function BaseStack({ className, alignItems, justifyContent, children, as }: BaseStackProps) { const Component = as ?? 'div'; diff --git a/src-web/hooks/useCreateRequest.ts b/src-web/hooks/useCreateRequest.ts index 5c3d3676..6450e7a3 100644 --- a/src-web/hooks/useCreateRequest.ts +++ b/src-web/hooks/useCreateRequest.ts @@ -7,7 +7,8 @@ import { useActiveWorkspace } from './useActiveWorkspace'; export function useCreateRequest({ navigateAfter }: { navigateAfter: boolean }) { const workspace = useActiveWorkspace(); const navigate = useNavigate(); - return useMutation>({ + + return useMutation>({ mutationFn: (patch) => { if (workspace === null) { throw new Error("Cannot create request when there's no active workspace"); diff --git a/src-web/hooks/useUpdateAnyRequest.ts b/src-web/hooks/useUpdateAnyRequest.ts new file mode 100644 index 00000000..525227a0 --- /dev/null +++ b/src-web/hooks/useUpdateAnyRequest.ts @@ -0,0 +1,26 @@ +import { useMutation } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import type { HttpRequest } from '../lib/models'; +import { useRequests } from './useRequests'; + +export function useUpdateAnyRequest() { + const requests = useRequests(); + return useMutation & { id: string }>({ + mutationFn: async (patch) => { + const request = requests.find((r) => r.id === patch.id) ?? null; + if (request === null) { + throw new Error("Can't update a null request"); + } + + const updatedRequest = { ...request, ...patch }; + + await invoke('update_request', { + request: { + ...updatedRequest, + createdAt: updatedRequest.createdAt.toISOString().replace('Z', ''), + updatedAt: updatedRequest.updatedAt.toISOString().replace('Z', ''), + }, + }); + }, + }); +} diff --git a/src-web/lib/models.ts b/src-web/lib/models.ts index 4d361938..e8404fee 100644 --- a/src-web/lib/models.ts +++ b/src-web/lib/models.ts @@ -18,6 +18,7 @@ export interface HttpHeader { export interface HttpRequest extends BaseModel { readonly model: 'http_request'; + sortPriority: number; name: string; url: string; body: string | null; diff --git a/src-web/main.css b/src-web/main.css index 665501e2..cafacb2c 100644 --- a/src-web/main.css +++ b/src-web/main.css @@ -23,12 +23,28 @@ cursor: default; } + .destroy-pointer-event, + .destroy-pointer-event * { + pointer-events: none !important; + } + /* Style the scrollbars */ ::-webkit-scrollbar-corner, ::-webkit-scrollbar { - @apply w-1.5 h-1.5 bg-gray-300/10; + @apply w-1.5 h-1.5; } + .scrollbar-track, + ::-webkit-scrollbar-corner, + ::-webkit-scrollbar { + @apply bg-gray-300/10; + } + + ::-webkit-scrollbar-thumb { + @apply hover:bg-gray-300 rounded-full; + } + + .scrollbar-thumb, ::-webkit-scrollbar-thumb { @apply bg-gray-200 hover:bg-gray-300 rounded-full; }