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