mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-22 16:48:30 +02:00
Re-order of pair editor
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Yaak App</title>
|
<title>Yaak App</title>
|
||||||
<!-- <script src="http://localhost:8097"></script>-->
|
<script src="http://localhost:8097"></script>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
|||||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -41,7 +41,8 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-helmet-async": "^1.3.0",
|
"react-helmet-async": "^1.3.0",
|
||||||
"react-router-dom": "^6.8.1",
|
"react-router-dom": "^6.8.1",
|
||||||
"react-use": "^17.4.0"
|
"react-use": "^17.4.0",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
|
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
|
||||||
@@ -50,6 +51,7 @@
|
|||||||
"@types/parse-color": "^1.0.1",
|
"@types/parse-color": "^1.0.1",
|
||||||
"@types/parse-json": "^4.0.0",
|
"@types/parse-json": "^4.0.0",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"@types/uuid": "^9.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.52.0",
|
"@typescript-eslint/eslint-plugin": "^5.52.0",
|
||||||
"@typescript-eslint/parser": "^5.52.0",
|
"@typescript-eslint/parser": "^5.52.0",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
@@ -2347,6 +2349,12 @@
|
|||||||
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
|
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/uuid": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "5.52.0",
|
"version": "5.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.52.0.tgz",
|
||||||
@@ -7650,7 +7658,6 @@
|
|||||||
"version": "9.0.0",
|
"version": "9.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
||||||
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
|
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/bin/uuid"
|
||||||
}
|
}
|
||||||
@@ -9639,6 +9646,12 @@
|
|||||||
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
|
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/uuid": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@typescript-eslint/eslint-plugin": {
|
"@typescript-eslint/eslint-plugin": {
|
||||||
"version": "5.52.0",
|
"version": "5.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.52.0.tgz",
|
||||||
@@ -13463,8 +13476,7 @@
|
|||||||
"uuid": {
|
"uuid": {
|
||||||
"version": "9.0.0",
|
"version": "9.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
||||||
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
|
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"vite": {
|
"vite": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
|
|||||||
@@ -48,7 +48,8 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-helmet-async": "^1.3.0",
|
"react-helmet-async": "^1.3.0",
|
||||||
"react-router-dom": "^6.8.1",
|
"react-router-dom": "^6.8.1",
|
||||||
"react-use": "^17.4.0"
|
"react-use": "^17.4.0",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
|
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
|
||||||
@@ -57,6 +58,7 @@
|
|||||||
"@types/parse-color": "^1.0.1",
|
"@types/parse-color": "^1.0.1",
|
||||||
"@types/parse-json": "^4.0.0",
|
"@types/parse-json": "^4.0.0",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"@types/uuid": "^9.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.52.0",
|
"@typescript-eslint/eslint-plugin": "^5.52.0",
|
||||||
"@typescript-eslint/parser": "^5.52.0",
|
"@typescript-eslint/parser": "^5.52.0",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
|||||||
import { invoke } from '@tauri-apps/api';
|
import { invoke } from '@tauri-apps/api';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
import { MotionConfig } from 'framer-motion';
|
import { MotionConfig } from 'framer-motion';
|
||||||
|
import { DndProvider } from 'react-dnd';
|
||||||
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import { HelmetProvider } from 'react-helmet-async';
|
import { HelmetProvider } from 'react-helmet-async';
|
||||||
import { matchPath } from 'react-router-dom';
|
import { matchPath } from 'react-router-dom';
|
||||||
import { keyValueQueryKey } from '../hooks/useKeyValue';
|
import { keyValueQueryKey } from '../hooks/useKeyValue';
|
||||||
@@ -123,8 +125,10 @@ export function App() {
|
|||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<MotionConfig transition={{ duration: 0.1 }}>
|
<MotionConfig transition={{ duration: 0.1 }}>
|
||||||
<HelmetProvider>
|
<HelmetProvider>
|
||||||
<AppRouter />
|
<DndProvider backend={HTML5Backend}>
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
<AppRouter />
|
||||||
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
|
</DndProvider>
|
||||||
</HelmetProvider>
|
</HelmetProvider>
|
||||||
</MotionConfig>
|
</MotionConfig>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
@@ -60,13 +60,14 @@ export function RequestPane({ fullHeight, className }: Props) {
|
|||||||
if (activeRequest === null) return null;
|
if (activeRequest === null) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classnames(className, 'p-2 grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1')}>
|
<div className={classnames(className, 'py-3 grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1')}>
|
||||||
<UrlBar request={activeRequest} />
|
<UrlBar className="pl-3" request={activeRequest} />
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTab.value}
|
value={activeTab.value}
|
||||||
onChangeValue={activeTab.set}
|
onChangeValue={activeTab.set}
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
|
tabListClassName="pl-3"
|
||||||
label="Request body"
|
label="Request body"
|
||||||
>
|
>
|
||||||
<TabContent value="headers">
|
<TabContent value="headers">
|
||||||
@@ -79,7 +80,7 @@ export function RequestPane({ fullHeight, className }: Props) {
|
|||||||
<TabContent value="params">
|
<TabContent value="params">
|
||||||
<ParametersEditor key={activeRequestId} parameters={[]} onChange={() => null} />
|
<ParametersEditor key={activeRequestId} parameters={[]} onChange={() => null} />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value="body">
|
<TabContent value="body" className="mt-1">
|
||||||
{activeRequest.bodyType === 'json' ? (
|
{activeRequest.bodyType === 'json' ? (
|
||||||
<Editor
|
<Editor
|
||||||
key={activeRequest.id}
|
key={activeRequest.id}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const ResponsePane = memo(function ResponsePane({ className }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classnames(className, 'p-2')}>
|
<div className={classnames(className, 'p-3')}>
|
||||||
<div
|
<div
|
||||||
className={classnames(
|
className={classnames(
|
||||||
'max-h-full h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 ',
|
'max-h-full h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 ',
|
||||||
@@ -113,7 +113,7 @@ export const ResponsePane = memo(function ResponsePane({ className }: Props) {
|
|||||||
|
|
||||||
{activeResponse?.error ? (
|
{activeResponse?.error ? (
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
<div className="text-white bg-red-500 px-3 py-2 rounded">{activeResponse.error}</div>
|
<div className="text-white bg-red-500 px-3 py-3 rounded">{activeResponse.error}</div>
|
||||||
</div>
|
</div>
|
||||||
) : viewMode === 'pretty' && contentType.includes('html') ? (
|
) : viewMode === 'pretty' && contentType.includes('html') ? (
|
||||||
<Webview body={activeResponse.body} contentType={contentType} url={activeResponse.url} />
|
<Webview body={activeResponse.body} contentType={contentType} url={activeResponse.url} />
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import type {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import React, { forwardRef, Fragment, memo, useCallback, useMemo, useRef, useState } from 'react';
|
import React, { forwardRef, Fragment, memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import type { XYCoord } from 'react-dnd';
|
import type { XYCoord } from 'react-dnd';
|
||||||
import { DndProvider, useDrag, useDragLayer, useDrop } from 'react-dnd';
|
import { useDrag, useDragLayer, useDrop } from 'react-dnd';
|
||||||
import { getEmptyImage, HTML5Backend } from 'react-dnd-html5-backend';
|
import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||||
import { useCreateRequest } from '../hooks/useCreateRequest';
|
import { useCreateRequest } from '../hooks/useCreateRequest';
|
||||||
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
||||||
@@ -39,15 +39,7 @@ enum ItemTypes {
|
|||||||
REQUEST = 'request',
|
REQUEST = 'request',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({ className }: Props) {
|
export const Sidebar = memo(function Sidebar({ className }: Props) {
|
||||||
return (
|
|
||||||
<DndProvider backend={HTML5Backend}>
|
|
||||||
<Container className={className} />
|
|
||||||
</DndProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Container({ className }: Props) {
|
|
||||||
const [isResizing, setIsRisizing] = useState<boolean>(false);
|
const [isResizing, setIsRisizing] = useState<boolean>(false);
|
||||||
const width = useKeyValue<number>({ key: 'sidebar_width', initialValue: INITIAL_WIDTH });
|
const width = useKeyValue<number>({ key: 'sidebar_width', initialValue: INITIAL_WIDTH });
|
||||||
const sidebarRef = useRef<HTMLDivElement>(null);
|
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -94,13 +86,13 @@ export function Container({ className }: Props) {
|
|||||||
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
|
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
|
||||||
<div
|
<div
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className="group absolute -right-2 top-0 bottom-0 w-4 cursor-ew-resize flex justify-center"
|
className="group absolute z-10 right-0 w-1 top-0 bottom-0 cursor-ew-resize flex justify-end"
|
||||||
onMouseDown={handleResizeStart}
|
onMouseDown={handleResizeStart}
|
||||||
onDoubleClick={handleResizeReset}
|
onDoubleClick={handleResizeReset}
|
||||||
>
|
>
|
||||||
<div // drag-divider
|
<div // drag-divider
|
||||||
className={classnames(
|
className={classnames(
|
||||||
'transition-colors w-[1px] group-hover:bg-gray-300 h-full pointer-events-none',
|
'transition-colors w-0.5 group-hover:bg-gray-300 h-full pointer-events-none',
|
||||||
isResizing && '!bg-blue-500/70',
|
isResizing && '!bg-blue-500/70',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -143,7 +135,7 @@ export function Container({ className }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
function SidebarItems({
|
function SidebarItems({
|
||||||
requests: unorderedRequests,
|
requests: unorderedRequests,
|
||||||
@@ -169,8 +161,6 @@ function SidebarItems({
|
|||||||
[requests],
|
[requests],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCancel = useCallback(() => setHoveredIndex(null), []);
|
|
||||||
|
|
||||||
const handleEnd = useCallback<DraggableSidebarItemProps['onEnd']>(
|
const handleEnd = useCallback<DraggableSidebarItemProps['onEnd']>(
|
||||||
(requestId) => {
|
(requestId) => {
|
||||||
if (hoveredIndex === null) return;
|
if (hoveredIndex === null) return;
|
||||||
@@ -181,11 +171,8 @@ function SidebarItems({
|
|||||||
if (request === undefined) return;
|
if (request === undefined) return;
|
||||||
|
|
||||||
const newRequests = requests.filter((r) => r.id !== requestId);
|
const newRequests = requests.filter((r) => r.id !== requestId);
|
||||||
if (hoveredIndex > index) {
|
if (hoveredIndex > index) newRequests.splice(hoveredIndex - 1, 0, request);
|
||||||
newRequests.splice(hoveredIndex - 1, 0, request);
|
else newRequests.splice(hoveredIndex, 0, request);
|
||||||
} else {
|
|
||||||
newRequests.splice(hoveredIndex, 0, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
const beforePriority = newRequests[hoveredIndex - 1]?.sortPriority ?? 0;
|
const beforePriority = newRequests[hoveredIndex - 1]?.sortPriority ?? 0;
|
||||||
const afterPriority = newRequests[hoveredIndex + 1]?.sortPriority ?? 0;
|
const afterPriority = newRequests[hoveredIndex + 1]?.sortPriority ?? 0;
|
||||||
@@ -220,7 +207,6 @@ function SidebarItems({
|
|||||||
sidebarWidth={sidebarWidth}
|
sidebarWidth={sidebarWidth}
|
||||||
onMove={handleMove}
|
onMove={handleMove}
|
||||||
onEnd={handleEnd}
|
onEnd={handleEnd}
|
||||||
onCancel={handleCancel}
|
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
@@ -369,7 +355,6 @@ const SidebarItem = memo(_SidebarItem);
|
|||||||
type DraggableSidebarItemProps = SidebarItemProps & {
|
type DraggableSidebarItemProps = SidebarItemProps & {
|
||||||
onMove: (id: string, side: 'above' | 'below') => void;
|
onMove: (id: string, side: 'above' | 'below') => void;
|
||||||
onEnd: (id: string) => void;
|
onEnd: (id: string) => void;
|
||||||
onCancel: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type DragItem = {
|
type DragItem = {
|
||||||
@@ -386,23 +371,23 @@ const DraggableSidebarItem = memo(function DraggableSidebarItem({
|
|||||||
sidebarWidth,
|
sidebarWidth,
|
||||||
onMove,
|
onMove,
|
||||||
onEnd,
|
onEnd,
|
||||||
onCancel,
|
|
||||||
}: DraggableSidebarItemProps) {
|
}: DraggableSidebarItemProps) {
|
||||||
const ref = useRef<HTMLLIElement>(null);
|
const ref = useRef<HTMLLIElement>(null);
|
||||||
|
|
||||||
const [, connectDrop] = useDrop<DragItem, void>({
|
const [, connectDrop] = useDrop<DragItem, void>(
|
||||||
accept: ItemTypes.REQUEST,
|
{
|
||||||
collect: (m) => ({ handlerId: m.getHandlerId(), isOver: m.isOver() }),
|
accept: ItemTypes.REQUEST,
|
||||||
hover: (item, monitor) => {
|
hover: (item, monitor) => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
const hoverBoundingRect = ref.current?.getBoundingClientRect();
|
const hoverBoundingRect = ref.current?.getBoundingClientRect();
|
||||||
|
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||||
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
const clientOffset = monitor.getClientOffset();
|
||||||
const clientOffset = monitor.getClientOffset();
|
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;
|
||||||
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;
|
onMove(requestId, hoverClientY < hoverMiddleY ? 'above' : 'below');
|
||||||
onMove(requestId, hoverClientY < hoverMiddleY ? 'above' : 'below');
|
},
|
||||||
},
|
},
|
||||||
});
|
[onMove],
|
||||||
|
);
|
||||||
|
|
||||||
const [{ isDragging }, connectDrag, preview] = useDrag<
|
const [{ isDragging }, connectDrag, preview] = useDrag<
|
||||||
DragItem,
|
DragItem,
|
||||||
@@ -414,10 +399,7 @@ const DraggableSidebarItem = memo(function DraggableSidebarItem({
|
|||||||
item: () => ({ id: requestId, requestName, workspaceId }),
|
item: () => ({ id: requestId, requestName, workspaceId }),
|
||||||
collect: (m) => ({ isDragging: m.isDragging() }),
|
collect: (m) => ({ isDragging: m.isDragging() }),
|
||||||
options: { dropEffect: 'move' },
|
options: { dropEffect: 'move' },
|
||||||
end: () => {
|
end: () => onEnd(requestId),
|
||||||
// TODO: Call cancel if dropped outside of sidebar
|
|
||||||
onEnd(requestId);
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
[onEnd],
|
[onEnd],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { memo, useCallback } from 'react';
|
import classnames from 'classnames';
|
||||||
|
import type { FormEvent } from 'react';
|
||||||
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
|
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
|
||||||
import { useSendRequest } from '../hooks/useSendRequest';
|
import { useSendRequest } from '../hooks/useSendRequest';
|
||||||
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
||||||
@@ -9,30 +11,31 @@ import { RequestMethodDropdown } from './RequestMethodDropdown';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
request: HttpRequest;
|
request: HttpRequest;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UrlBar = memo(function UrlBar({ request }: Props) {
|
export const UrlBar = memo(function UrlBar({ request, className }: Props) {
|
||||||
const sendRequest = useSendRequest(request.id);
|
const sendRequest = useSendRequest(request.id);
|
||||||
const updateRequest = useUpdateRequest(request.id);
|
const updateRequest = useUpdateRequest(request.id);
|
||||||
const handleMethodChange = useCallback((method: string) => updateRequest.mutate({ method }), []);
|
const handleMethodChange = useCallback((method: string) => updateRequest.mutate({ method }), []);
|
||||||
const handleUrlChange = useCallback((url: string) => updateRequest.mutate({ url }), []);
|
const handleUrlChange = useCallback((url: string) => updateRequest.mutate({ url }), []);
|
||||||
const loading = useIsResponseLoading(request.id);
|
const loading = useIsResponseLoading(request.id);
|
||||||
|
const useEditor = useMemo(() => ({ useTemplating: true, contentType: 'url' }), []);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(
|
||||||
|
async (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendRequest();
|
||||||
|
},
|
||||||
|
[sendRequest],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form onSubmit={handleSubmit} className={classnames(className, 'w-full flex items-center')}>
|
||||||
onSubmit={async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendRequest();
|
|
||||||
}}
|
|
||||||
className="w-full flex items-center"
|
|
||||||
>
|
|
||||||
<Input
|
<Input
|
||||||
key={request.id}
|
key={request.id}
|
||||||
hideLabel
|
hideLabel
|
||||||
useEditor={{
|
useEditor={useEditor}
|
||||||
useTemplating: true,
|
|
||||||
contentType: 'url',
|
|
||||||
autocompleteOptions: [{ label: 'FOO', type: 'constant' }],
|
|
||||||
}}
|
|
||||||
className="px-0"
|
className="px-0"
|
||||||
name="url"
|
name="url"
|
||||||
label="Enter URL"
|
label="Enter URL"
|
||||||
|
|||||||
@@ -72,9 +72,9 @@ export default function Workspace() {
|
|||||||
>
|
>
|
||||||
<RequestPane
|
<RequestPane
|
||||||
fullHeight={isSideBySide}
|
fullHeight={isSideBySide}
|
||||||
className={classnames(isSideBySide ? 'pr-1' : 'pr-2')}
|
className={classnames(isSideBySide ? 'pr-1.5' : 'pr-3')}
|
||||||
/>
|
/>
|
||||||
<ResponsePane className={classnames(isSideBySide && 'pl-1')} />
|
<ResponsePane className={classnames(isSideBySide && 'pl-1.5')} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Cross2Icon,
|
Cross2Icon,
|
||||||
DotsHorizontalIcon,
|
DotsHorizontalIcon,
|
||||||
DotsVerticalIcon,
|
DotsVerticalIcon,
|
||||||
|
DragHandleDots2Icon,
|
||||||
EyeOpenIcon,
|
EyeOpenIcon,
|
||||||
GearIcon,
|
GearIcon,
|
||||||
HomeIcon,
|
HomeIcon,
|
||||||
@@ -39,6 +40,7 @@ const icons = {
|
|||||||
colorWheel: ColorWheelIcon,
|
colorWheel: ColorWheelIcon,
|
||||||
dotsH: DotsHorizontalIcon,
|
dotsH: DotsHorizontalIcon,
|
||||||
dotsV: DotsVerticalIcon,
|
dotsV: DotsVerticalIcon,
|
||||||
|
drag: DragHandleDots2Icon,
|
||||||
eye: EyeOpenIcon,
|
eye: EyeOpenIcon,
|
||||||
gear: GearIcon,
|
gear: GearIcon,
|
||||||
home: HomeIcon,
|
home: HomeIcon,
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import type { XYCoord } from 'react-dnd';
|
||||||
|
import { useDrag, useDrop } from 'react-dnd';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
import type { GenericCompletionConfig } from './Editor/genericCompletion';
|
import type { GenericCompletionConfig } from './Editor/genericCompletion';
|
||||||
|
import { Icon } from './Icon';
|
||||||
import { IconButton } from './IconButton';
|
import { IconButton } from './IconButton';
|
||||||
import { Input } from './Input';
|
import { Input } from './Input';
|
||||||
import { VStack } from './Stacks';
|
|
||||||
|
|
||||||
export type PairEditorProps = {
|
export type PairEditorProps = {
|
||||||
pairs: Pair[];
|
pairs: Pair[];
|
||||||
@@ -34,10 +37,11 @@ export const PairEditor = memo(function PairEditor({
|
|||||||
className,
|
className,
|
||||||
onChange,
|
onChange,
|
||||||
}: PairEditorProps) {
|
}: PairEditorProps) {
|
||||||
|
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||||
const [pairs, setPairs] = useState<PairContainer[]>(() => {
|
const [pairs, setPairs] = useState<PairContainer[]>(() => {
|
||||||
// Remove empty headers on initial render
|
// Remove empty headers on initial render
|
||||||
const nonEmpty = originalPairs.filter((h) => !(h.name === '' && h.value === ''));
|
const nonEmpty = originalPairs.filter((h) => !(h.name === '' && h.value === ''));
|
||||||
const pairs = nonEmpty.map((h) => ({ pair: h, id: Math.random().toString() }));
|
const pairs = nonEmpty.map((pair) => newPairContainer(pair));
|
||||||
return [...pairs, newPairContainer()];
|
return [...pairs, newPairContainer()];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,6 +56,33 @@ export const PairEditor = memo(function PairEditor({
|
|||||||
[onChange],
|
[onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleMove = useCallback<FormRowProps['onMove']>(
|
||||||
|
(id, side) => {
|
||||||
|
const dragIndex = pairs.findIndex((r) => r.id === id);
|
||||||
|
setHoveredIndex(side === 'above' ? dragIndex : dragIndex + 1);
|
||||||
|
},
|
||||||
|
[pairs],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleEnd = useCallback<FormRowProps['onEnd']>(
|
||||||
|
(id: string) => {
|
||||||
|
if (hoveredIndex === null) return;
|
||||||
|
setHoveredIndex(null);
|
||||||
|
|
||||||
|
setPairsAndSave((pairs) => {
|
||||||
|
const index = pairs.findIndex((p) => p.id === id);
|
||||||
|
const pair = pairs[index];
|
||||||
|
if (pair === undefined) return pairs;
|
||||||
|
|
||||||
|
const newPairs = pairs.filter((p) => p.id !== id);
|
||||||
|
if (hoveredIndex > index) newPairs.splice(hoveredIndex - 1, 0, pair);
|
||||||
|
else newPairs.splice(hoveredIndex, 0, pair);
|
||||||
|
return newPairs;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[hoveredIndex],
|
||||||
|
);
|
||||||
|
|
||||||
const handleChangeHeader = useCallback((pair: PairContainer) => {
|
const handleChangeHeader = useCallback((pair: PairContainer) => {
|
||||||
setPairsAndSave((pairs) => pairs.map((p) => (pair.id !== p.id ? p : pair)));
|
setPairsAndSave((pairs) => pairs.map((p) => (pair.id !== p.id ? p : pair)));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -78,13 +109,20 @@ export const PairEditor = memo(function PairEditor({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classnames(className, 'pb-6 grid')}>
|
<div
|
||||||
<VStack space={2}>
|
className={classnames(
|
||||||
{pairs.map((p, i) => {
|
className,
|
||||||
const isLast = i === pairs.length - 1;
|
'pb-6 grid',
|
||||||
return (
|
// NOTE: Add padding to top so overflow doesn't hide drop marker
|
||||||
|
'py-1',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{pairs.map((p, i) => {
|
||||||
|
const isLast = i === pairs.length - 1;
|
||||||
|
return (
|
||||||
|
<Fragment key={p.id}>
|
||||||
|
{hoveredIndex === i && <DropMarker />}
|
||||||
<FormRow
|
<FormRow
|
||||||
key={p.id}
|
|
||||||
pairContainer={p}
|
pairContainer={p}
|
||||||
isLast={isLast}
|
isLast={isLast}
|
||||||
onChange={handleChangeHeader}
|
onChange={handleChangeHeader}
|
||||||
@@ -94,16 +132,25 @@ export const PairEditor = memo(function PairEditor({
|
|||||||
valuePlaceholder={valuePlaceholder}
|
valuePlaceholder={valuePlaceholder}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onDelete={isLast ? undefined : handleDelete}
|
onDelete={isLast ? undefined : handleDelete}
|
||||||
|
onEnd={handleEnd}
|
||||||
|
onMove={handleMove}
|
||||||
/>
|
/>
|
||||||
);
|
{hoveredIndex === pairs.length && <DropMarker />}
|
||||||
})}
|
</Fragment>
|
||||||
</VStack>
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
enum ItemTypes {
|
||||||
|
ROW = 'pair-row',
|
||||||
|
}
|
||||||
|
|
||||||
type FormRowProps = {
|
type FormRowProps = {
|
||||||
pairContainer: PairContainer;
|
pairContainer: PairContainer;
|
||||||
|
onMove: (id: string, side: 'above' | 'below') => void;
|
||||||
|
onEnd: (id: string) => void;
|
||||||
onChange: (pair: PairContainer) => void;
|
onChange: (pair: PairContainer) => void;
|
||||||
onDelete?: (pair: PairContainer) => void;
|
onDelete?: (pair: PairContainer) => void;
|
||||||
onFocus?: (pair: PairContainer) => void;
|
onFocus?: (pair: PairContainer) => void;
|
||||||
@@ -118,6 +165,8 @@ const FormRow = memo(function FormRow({
|
|||||||
onChange,
|
onChange,
|
||||||
onDelete,
|
onDelete,
|
||||||
onFocus,
|
onFocus,
|
||||||
|
onMove,
|
||||||
|
onEnd,
|
||||||
isLast,
|
isLast,
|
||||||
nameAutocomplete,
|
nameAutocomplete,
|
||||||
valueAutocomplete,
|
valueAutocomplete,
|
||||||
@@ -125,6 +174,7 @@ const FormRow = memo(function FormRow({
|
|||||||
valuePlaceholder,
|
valuePlaceholder,
|
||||||
}: FormRowProps) {
|
}: FormRowProps) {
|
||||||
const { id } = pairContainer;
|
const { id } = pairContainer;
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const handleChangeName = useMemo(
|
const handleChangeName = useMemo(
|
||||||
() => (name: string) => onChange({ id, pair: { name, value: pairContainer.pair.value } }),
|
() => (name: string) => onChange({ id, pair: { name, value: pairContainer.pair.value } }),
|
||||||
@@ -149,8 +199,51 @@ const FormRow = memo(function FormRow({
|
|||||||
const handleFocus = useCallback(() => onFocus?.(pairContainer), [onFocus, pairContainer]);
|
const handleFocus = useCallback(() => onFocus?.(pairContainer), [onFocus, pairContainer]);
|
||||||
const handleDelete = useCallback(() => onDelete?.(pairContainer), [onDelete, pairContainer]);
|
const handleDelete = useCallback(() => onDelete?.(pairContainer), [onDelete, pairContainer]);
|
||||||
|
|
||||||
|
const [, connectDrop] = useDrop<PairContainer>(
|
||||||
|
{
|
||||||
|
accept: ItemTypes.ROW,
|
||||||
|
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(pairContainer.id, hoverClientY < hoverMiddleY ? 'above' : 'below');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[onMove],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [, connectDrag] = useDrag<PairContainer>(
|
||||||
|
{
|
||||||
|
type: ItemTypes.ROW,
|
||||||
|
item: () => pairContainer,
|
||||||
|
collect: (m) => ({ isDragging: m.isDragging() }),
|
||||||
|
end: () => onEnd(pairContainer.id),
|
||||||
|
},
|
||||||
|
[pairContainer, onEnd],
|
||||||
|
);
|
||||||
|
|
||||||
|
connectDrag(ref);
|
||||||
|
connectDrop(ref);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] grid-rows-1 gap-2 items-center">
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="pb-2 group grid grid-cols-[auto_minmax(0,1fr)_minmax(0,1fr)_auto] grid-rows-1 gap-2 items-center"
|
||||||
|
>
|
||||||
|
{!isLast ? (
|
||||||
|
<div
|
||||||
|
className={classnames(
|
||||||
|
'-mr-2 py-2 h-9 w-3 flex items-center',
|
||||||
|
'justify-center opacity-0 hover:opacity-100',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon icon="drag" className="pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="w-1" />
|
||||||
|
)}
|
||||||
<Input
|
<Input
|
||||||
hideLabel
|
hideLabel
|
||||||
containerClassName={classnames(isLast && 'border-dashed')}
|
containerClassName={classnames(isLast && 'border-dashed')}
|
||||||
@@ -188,6 +281,17 @@ const FormRow = memo(function FormRow({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const newPairContainer = (): PairContainer => {
|
const newPairContainer = (pair?: Pair): PairContainer => {
|
||||||
return { pair: { name: '', value: '' }, id: Math.random().toString() };
|
return { pair: pair ?? { name: '', value: '' }, id: uuid() };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DropMarker = memo(
|
||||||
|
function DropMarker() {
|
||||||
|
return (
|
||||||
|
<div className="relative w-full h-0 overflow-visible pointer-events-none">
|
||||||
|
<div className="absolute z-50 left-0 right-0 bottom-[1px] h-[0.2em] bg-blue-500/50 rounded-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
() => true,
|
||||||
|
);
|
||||||
|
|||||||
@@ -47,10 +47,7 @@ export const Tabs = memo(function Tabs({
|
|||||||
>
|
>
|
||||||
<T.List
|
<T.List
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
className={classnames(
|
className={classnames(tabListClassName, 'h-auto flex items-center overflow-x-auto pb-1')}
|
||||||
tabListClassName,
|
|
||||||
'h-auto flex items-center overflow-x-auto pb-1 mb-1',
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<HStack space={1}>
|
<HStack space={1}>
|
||||||
{tabs.map((t) => {
|
{tabs.map((t) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user