From 64e8340040eb1db644effdc68839e7c43ea63188 Mon Sep 17 00:00:00 2001 From: Jonathan Roesner Date: Thu, 24 Oct 2024 15:36:09 +0200 Subject: [PATCH] WIP --- .../src/components/bodyEditor/BodyEditor.tsx | 3 + .../components/bodyEditor/BodyTextEditor.tsx | 10 +- .../src/components/cmdPalette/CmdPalette.tsx | 316 ++++++++++++++++-- .../collectionPanel/CollectionPanel.tsx | 2 +- .../collectionView/MoveableHeader.tsx | 40 ++- client/src/components/editor/Editor.tsx | 5 +- .../components/requestPanel/RequestPanel.tsx | 8 + .../components/requestPanel/RequestSender.tsx | 6 + .../src/components/sidebar/Sidebar.module.css | 2 +- client/src/components/uriBar/UriBar.tsx | 15 +- client/src/context/CurrentModalContext.tsx | 50 +++ client/src/context/FocusElementsContext.tsx | 58 ++++ client/src/context/index.tsx | 10 +- client/src/index.css | 5 + client/src/pages/dashboard/Dashboard.tsx | 93 +++++- client/src/state/collections.tsx | 21 ++ client/src/state/openHistory.tsx | 92 +++++ .../yaade/server/routes/CollectionRoute.kt | 1 + 18 files changed, 666 insertions(+), 71 deletions(-) create mode 100644 client/src/context/CurrentModalContext.tsx create mode 100644 client/src/context/FocusElementsContext.tsx create mode 100644 client/src/state/openHistory.tsx diff --git a/client/src/components/bodyEditor/BodyEditor.tsx b/client/src/components/bodyEditor/BodyEditor.tsx index 5dc0d56a..a7401817 100644 --- a/client/src/components/bodyEditor/BodyEditor.tsx +++ b/client/src/components/bodyEditor/BodyEditor.tsx @@ -17,6 +17,7 @@ type BodyEditorProps = { contentType: string; setContentType: any; setContentTypeHeader: any; + setEditorView?: any; }; function BodyEditor({ @@ -28,6 +29,7 @@ function BodyEditor({ contentType, setContentType, setContentTypeHeader, + setEditorView, }: BodyEditorProps) { const toast = useToast(); function handleBeautifyClick() { @@ -71,6 +73,7 @@ function BodyEditor({ setContent={setContent} selectedEnv={selectedEnv} contentType={contentType} + setEditorView={setEditorView} /> ); } diff --git a/client/src/components/bodyEditor/BodyTextEditor.tsx b/client/src/components/bodyEditor/BodyTextEditor.tsx index c268d6a1..798668f0 100644 --- a/client/src/components/bodyEditor/BodyTextEditor.tsx +++ b/client/src/components/bodyEditor/BodyTextEditor.tsx @@ -22,6 +22,7 @@ type BodyTextEditorProps = { setContent: any; selectedEnv: any; contentType: string; + setEditorView?: any; }; function BodyTextEditor({ @@ -29,6 +30,7 @@ function BodyTextEditor({ setContent, selectedEnv, contentType, + setEditorView, }: BodyTextEditorProps) { const { colorMode } = useColorMode(); @@ -51,7 +53,7 @@ function BodyTextEditor({ const ref = useRef(null); - const { setContainer } = useCodeMirror({ + const { setContainer, view } = useCodeMirror({ container: ref.current, onChange: (value: string) => setContent(value), extensions: [extensions], @@ -60,6 +62,12 @@ function BodyTextEditor({ style: { height: '100%' }, }); + useEffect(() => { + if (view && setEditorView) { + setEditorView(view); + } + }, [view, setEditorView]); + useEffect(() => { if (ref.current) { setContainer(ref.current); diff --git a/client/src/components/cmdPalette/CmdPalette.tsx b/client/src/components/cmdPalette/CmdPalette.tsx index e533f9ca..645d080f 100644 --- a/client/src/components/cmdPalette/CmdPalette.tsx +++ b/client/src/components/cmdPalette/CmdPalette.tsx @@ -1,69 +1,321 @@ -import { Dispatch, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import CommandPalette, { Command } from 'react-command-palette'; +import { CurrentModalContext } from '../../context/CurrentModalContext'; +import { FocusElementsContext } from '../../context/FocusElementsContext'; import Collection, { CurrentCollection } from '../../model/Collection'; -import { CurrentRequest } from '../../model/Request'; +import Request, { CurrentRequest } from '../../model/Request'; +import Script, { CurrentScript } from '../../model/Script'; +import { + OpenHistory, + OpenHistoryAction, + OpenHistoryActionType, +} from '../../state/openHistory'; import { useKeyPress } from '../../utils/useKeyPress'; type CmdPaletteProps = { collections: Collection[]; currentRequest?: CurrentRequest; currentCollection?: CurrentCollection; + currentScript?: CurrentScript; selectCollection: any; + selectRequest: any; + selectScript: any; setCollectionPanelTabIndex: (index: number) => void; + setRequestPanelTabIndex: (index: number) => void; + openHistory: OpenHistory; + dispatchOpenHistory: React.Dispatch; }; function CmdPalette({ collections, currentRequest, currentCollection, + currentScript, selectCollection, + selectRequest, + selectScript, setCollectionPanelTabIndex, + setRequestPanelTabIndex, + openHistory, + dispatchOpenHistory, }: CmdPaletteProps) { const [isOpen, setIsOpen] = useState(false); - const [search, setSearch] = useState(''); - useKeyPress(() => setIsOpen(true), 'p', true, true); + const { setCurrentModal: setModal } = useContext(CurrentModalContext); + const { + uriBarView, + bodyEditorView, + requestScriptEditorView, + responseScriptEditorView, + i, + } = useContext(FocusElementsContext); + useKeyPress(() => setIsOpen((open) => !open), 'p', true, true); + + function getCurrentCollectionId() { + return ( + currentCollection?.id || currentRequest?.collectionId || currentScript?.collectionId + ); + } async function openCollectionPanelTab(index: number) { - const collectionId = currentCollection?.id || currentRequest?.collectionId; + const collectionId = getCurrentCollectionId(); if (!collectionId) return; + await selectCollection.current(collectionId); + setCollectionPanelTabIndex(index); + } - if (collectionId !== currentCollection?.id) { - await selectCollection.current(collectionId); + async function openRequestPanelTab(index: number, focusElement?: any) { + const requestId = currentRequest?.id; + if (!requestId) return; + await selectRequest.current(requestId); + setRequestPanelTabIndex(index); + if (focusElement) { + focusElement.focus(); } - - setCollectionPanelTabIndex(index); } - const commands: Command[] = [ - { - id: 0, - name: 'Open Current Collection', - async command() { - if (!currentRequest) return; - await selectCollection.current(currentRequest.collectionId); + const commands = []; + + if ( + currentCollection?.id || + (currentRequest?.id && currentRequest?.id !== -1) || + currentScript?.id + ) { + const collectionCommands: Command[] = [ + { + id: commands.length, + name: 'Open Current Collection', + command: () => openCollectionPanelTab(0), + color: '', + }, + { + id: commands.length, + name: 'Open Current Environment', + command: () => openCollectionPanelTab(1), + color: '', + }, + { + id: commands.length, + name: 'Open Collection Headers', + command: () => openCollectionPanelTab(2), + color: '', }, - color: 'red', - }, - { - id: 1, - name: 'Open Current Environment', - async command() { - await openCollectionPanelTab(1); + { + id: commands.length, + name: 'Open Collection Auth', + command: () => openCollectionPanelTab(3), + color: '', + }, + { + id: commands.length, + name: 'Open Collection Request Script', + command: () => openCollectionPanelTab(4), + color: '', + }, + { + id: commands.length, + name: 'Open Collection Response Script', + command: () => openCollectionPanelTab(5), + color: '', + }, + { + id: commands.length, + name: 'Open Collection Settings', + command: () => openCollectionPanelTab(6), + color: '', + }, + { + id: commands.length, + name: 'New Request', + command: () => { + setModal('newRequest', 'collection', getCurrentCollectionId() ?? 0); + }, + color: '', + }, + ]; + commands.push(...collectionCommands); + } + + if (openHistory.index !== openHistory.items.length - 1) { + commands.push({ + id: commands.length, + name: 'History: Forward', + command: () => { + const item = openHistory.items[openHistory.index + 1]; + if (!item) { + return; + } + dispatchOpenHistory({ type: OpenHistoryActionType.FORWARD }); + if (item.type === 'request') { + selectRequest.current(item.id, false); + } else if (item.type === 'collection') { + selectCollection.current(item.id, false); + } else if (item.type === 'script') { + selectScript.current(item.id, false); + } }, color: '', - }, - { - id: 2, - name: 'Open Collection Headers', - async command() { - await openCollectionPanelTab(2); + }); + } + + if (openHistory.index > 0) { + commands.push({ + id: commands.length, + name: 'History: Backward', + command: () => { + const item = openHistory.items[openHistory.index - 1]; + if (!item) { + return; + } + dispatchOpenHistory({ type: OpenHistoryActionType.BACKWARD }); + if (item.type === 'request') { + selectRequest.current(item.id, false); + } else if (item.type === 'collection') { + selectCollection.current(item.id, false); + } else if (item.type === 'script') { + selectScript.current(item.id, false); + } }, color: '', - }, - ]; + }); + } + + if (currentRequest) { + const requestCommands = [ + // { + // id: commands.length, + // name: 'Edit Request Description', + // command: () => openRequestPanelTab(0), + // color: '', + // }, + // { + // id: commands.length, + // name: 'Edit Request Parameters', + // command: () => openRequestPanelTab(1), + // color: '', + // }, + // { + // id: commands.length, + // name: 'Edit Request Headers', + // command: () => openRequestPanelTab(2), + // color: '', + // }, + // { + // id: commands.length, + // name: 'Edit Request Body', + // command: () => openRequestPanelTab(3, bodyEditorView), + // color: '', + // }, + // { + // id: commands.length, + // name: 'Edit Request Auth', + // command: () => openRequestPanelTab(4), + // color: '', + // }, + // { + // id: commands.length, + // name: 'Edit Request Script', + // command: () => openRequestPanelTab(5, requestScriptEditorView), + // color: '', + // }, + // { + // id: commands.length, + // name: 'Edit Response Script', + // command: () => openRequestPanelTab(6, responseScriptEditorView), + // color: '', + // }, + // { + // id: commands.length, + // name: 'Open Request Code', + // command: () => openRequestPanelTab(7), + // color: '', + // }, + { + id: commands.length, + name: 'Edit Request URL', + command: () => { + console.log('edit request url', uriBarView); + if (uriBarView) { + uriBarView.focus(); + } + }, + color: '', + }, + ]; + commands.push(...requestCommands); + } + + function getFlattenedCollections(collections: Collection[]): Collection[] { + if (!collections) return []; + const res = [...collections]; + for (const c of collections) { + res.push(...getFlattenedCollections(c.children)); + } + return res; + } + function getFlattenedRequests(collections: Collection[]): Request[] { + return getFlattenedCollections(collections).flatMap((c) => c.requests ?? []); + } + function getFlattenedScripts(collections: Collection[]): Script[] { + return getFlattenedCollections(collections).flatMap((c) => c.scripts ?? []); + } + + const openCollectionCommands = getFlattenedCollections(collections).map( + (c, i) => + ({ + id: commands.length + i, + name: `Open Collection: ${c.data.name} [${c.id}]`, + command: () => selectCollection.current(c.id), + color: '', + } as Command), + ); + + commands.push(...openCollectionCommands); + + const openRequestCommands = getFlattenedRequests(collections).map( + (r, i) => + ({ + id: commands.length + i, + name: `Open Request: ${r.data.name} [${r.id}]`, + command: () => selectRequest.current(r.id), + color: '', + } as Command), + ); + + commands.push(...openRequestCommands); + + const openScriptCommands = getFlattenedScripts(collections).map( + (s, i) => + ({ + id: commands.length + i, + name: `Open Script: ${s.data.name} [${s.id}]`, + command: () => selectScript.current(s.id), + color: '', + } as Command), + ); + + commands.push(...openScriptCommands); - return ; + return ( +
+ setIsOpen(false)} + resetInputOnOpen={true} + /> +
+ ); } export default CmdPalette; diff --git a/client/src/components/collectionPanel/CollectionPanel.tsx b/client/src/components/collectionPanel/CollectionPanel.tsx index 0185f01f..52b38a01 100644 --- a/client/src/components/collectionPanel/CollectionPanel.tsx +++ b/client/src/components/collectionPanel/CollectionPanel.tsx @@ -9,7 +9,7 @@ import { useColorMode, useToast, } from '@chakra-ui/react'; -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import { VscSave } from 'react-icons/vsc'; import { CollectionSettings, CurrentCollection } from '../../model/Collection'; diff --git a/client/src/components/collectionView/MoveableHeader.tsx b/client/src/components/collectionView/MoveableHeader.tsx index 329a459c..0182f326 100644 --- a/client/src/components/collectionView/MoveableHeader.tsx +++ b/client/src/components/collectionView/MoveableHeader.tsx @@ -31,6 +31,7 @@ import { import api from '../../api'; import { UserContext } from '../../context'; +import { CurrentModalContext } from '../../context/CurrentModalContext'; import Collection, { SidebarCollection } from '../../model/Collection'; import Request from '../../model/Request'; import Script from '../../model/Script'; @@ -64,7 +65,6 @@ type MoveableHeaderState = { name: string; newRequestName: string; importData: string; - currentModal: string; newCollectionName: string; groups: string[]; uploadFile: any; @@ -114,10 +114,23 @@ function MoveableHeader({ isCollectionDescendant, }: MoveableHeaderProps) { const { user } = useContext(UserContext); + const { + currentModalState, + currentModalType, + currentModalId, + clearCurrentModal, + setCurrentModal, + } = useContext(CurrentModalContext); + const isOpen = useMemo( + () => + !!currentModalState && + currentModalType === 'collection' && + currentModalId === collection.id, + [currentModalState, currentModalType, currentModalId, collection.id], + ); const [state, setState] = useState({ name: collection.name, newRequestName: '', - currentModal: '', importData: '', newCollectionName: '', groups: user?.data?.groups ?? [], @@ -135,7 +148,6 @@ function MoveableHeader({ const { colorMode } = useColorMode(); const { onCopy } = useClipboard(`${window.location.origin}/#/${collection.id}`); const toast = useToast(); - const { isOpen, onOpen, onClose } = useDisclosure(); const headerVariants = useMemo(() => { return currentCollectionId === collection.id ? ['selected'] : []; }, [currentCollectionId, collection.id]); @@ -362,7 +374,7 @@ function MoveableHeader({ newScriptName: '', importData: '', }); - onClose(); + clearCurrentModal(); } async function handleCreateRequestClick() { @@ -488,7 +500,7 @@ function MoveableHeader({ // correctly on response scripts const currentModal = !isOpen ? null - : ((s: string) => { + : ((s: string | undefined) => { switch (s) { case 'newRequest': return ( @@ -730,7 +742,7 @@ function MoveableHeader({ ); } - })(state.currentModal); + })(currentModalState); return (
} onClick={(e) => { e.stopPropagation(); - setState({ ...state, currentModal: 'newRequest' }); - onOpen(); + setCurrentModal('newRequest', 'collection', collection.id); }} > New Request @@ -810,8 +821,7 @@ function MoveableHeader({ icon={} onClick={(e) => { e.stopPropagation(); - setState({ ...state, currentModal: 'newCollection' }); - onOpen(); + setCurrentModal('newCollection', 'collection', collection.id); }} > New Collection @@ -820,8 +830,7 @@ function MoveableHeader({ icon={} onClick={(e) => { e.stopPropagation(); - setState({ ...state, currentModal: 'newScript' }); - onOpen(); + setCurrentModal('newScript', 'collection', collection.id); }} > New Job Script @@ -842,11 +851,9 @@ function MoveableHeader({ e.stopPropagation(); setState({ ...state, - currentModal: 'duplicate', name: `${collection.name} (copy)`, }); - - onOpen(); + setCurrentModal('duplicate', 'collection', collection.id); }} > Duplicate @@ -856,8 +863,7 @@ function MoveableHeader({ icon={} onClick={(e) => { e.stopPropagation(); - setState({ ...state, currentModal: 'delete' }); - onOpen(); + setCurrentModal('delete', 'collection', collection.id); }} > Delete diff --git a/client/src/components/editor/Editor.tsx b/client/src/components/editor/Editor.tsx index aca594a3..4a94a830 100644 --- a/client/src/components/editor/Editor.tsx +++ b/client/src/components/editor/Editor.tsx @@ -28,7 +28,7 @@ function Editor({ content, setContent }: EditorProps) { const ref = useRef(null); - const { setContainer } = useCodeMirror({ + const { setContainer, view } = useCodeMirror({ container: ref.current, onChange: (value: string) => setContent(value), extensions: [extensions], @@ -40,8 +40,9 @@ function Editor({ content, setContent }: EditorProps) { useEffect(() => { if (ref.current) { setContainer(ref.current); + view?.focus(); } - }, [ref, setContainer]); + }, [ref, setContainer, view]); function handleBeautifyClick() { try { diff --git a/client/src/components/requestPanel/RequestPanel.tsx b/client/src/components/requestPanel/RequestPanel.tsx index 21dea26f..8df7aa98 100644 --- a/client/src/components/requestPanel/RequestPanel.tsx +++ b/client/src/components/requestPanel/RequestPanel.tsx @@ -4,6 +4,7 @@ import { Dispatch, useCallback, useContext, useMemo } from 'react'; import { VscSave } from 'react-icons/vsc'; import { UserContext } from '../../context'; +import { FocusElementsContext } from '../../context/FocusElementsContext'; import KVRow from '../../model/KVRow'; import Request, { AuthData, CurrentRequest } from '../../model/Request'; import Response from '../../model/Response'; @@ -96,6 +97,8 @@ type RequestPanelProps = { saveOnSend: (request: Request) => Promise; handleSaveRequestClick: () => Promise; selectedEnv: Record; + tabIndex: number; + setTabIndex: (index: number) => void; }; function RequestPanel({ @@ -105,6 +108,8 @@ function RequestPanel({ saveOnSend, handleSaveRequestClick, selectedEnv, + tabIndex, + setTabIndex, }: RequestPanelProps) { const toast = useToast(); const { user } = useContext(UserContext); @@ -290,6 +295,7 @@ function RequestPanel({ handleSendButtonClick={handleSendButtonClick} isLoading={currentRequest.isLoading} env={selectedEnv} + currentRequestId={currentRequest.id} /> setTabIndex(index)} colorScheme="green" mt="1" display="flex" diff --git a/client/src/components/requestPanel/RequestSender.tsx b/client/src/components/requestPanel/RequestSender.tsx index f1799d9c..023363e8 100644 --- a/client/src/components/requestPanel/RequestSender.tsx +++ b/client/src/components/requestPanel/RequestSender.tsx @@ -47,6 +47,8 @@ type RequestSenderProps = { isExtInitialized: MutableRefObject; extVersion: MutableRefObject; openExtModal: () => void; + tabIndex: number; + setTabIndex: (index: number) => void; }; function RequestSender({ @@ -57,6 +59,8 @@ function RequestSender({ isExtInitialized, extVersion, openExtModal, + tabIndex, + setTabIndex, }: RequestSenderProps) { const [newReqForm, setNewReqForm] = useState({ collectionId: -1, @@ -548,6 +552,8 @@ function RequestSender({ sendRequest={sendRequest} saveOnSend={saveOnSend} selectedEnv={selectedEnv} + tabIndex={tabIndex} + setTabIndex={setTabIndex} /> )} void; env: any; + // NOTE: we need this to set the focus element on changing the request + currentRequestId: number; }; type MethodOptionProps = { @@ -42,11 +45,13 @@ function UriBar({ isLoading, handleSendButtonClick, env, + currentRequestId, }: UriBarProps) { const { colorMode } = useColorMode(); const ref = useRef(null); + const { setUriBarView } = useContext(FocusElementsContext); - const { setContainer } = useCodeMirror({ + const { setContainer, view } = useCodeMirror({ container: ref.current, onChange: (value: string) => setUri(value), extensions: [ @@ -67,6 +72,12 @@ function UriBar({ basicSetup: singleLineSetupOptions, }); + useEffect(() => { + if (view) { + setUriBarView(view); + } + }, [view, currentRequestId, setUriBarView]); + useEffect(() => { if (ref.current) { setContainer(ref.current); diff --git a/client/src/context/CurrentModalContext.tsx b/client/src/context/CurrentModalContext.tsx new file mode 100644 index 00000000..dc19f45a --- /dev/null +++ b/client/src/context/CurrentModalContext.tsx @@ -0,0 +1,50 @@ +import { createContext, FunctionComponent, useState } from 'react'; + +interface ICurrentModalContext { + currentModalState?: string; + currentModalType?: string; + currentModalId?: number; + setCurrentModal: (modal: string, type: string, id: number) => void; + clearCurrentModal: () => void; +} + +const CurrentModalContext = createContext({ + setCurrentModal: () => {}, + clearCurrentModal: () => {}, +}); + +type CurrentModalState = { + currentModalState?: string; + currentModalType?: string; + currentModalId?: number; +}; + +const CurrentModalProvider: FunctionComponent = ({ children }) => { + const [state, setState] = useState({}); + + return ( + { + setState({ currentModalState: currentModal, currentModalType, currentModalId }); + }, + clearCurrentModal: () => { + setState({}); + }, + }} + > + {children} + + ); +}; + +export { CurrentModalContext }; + +export default CurrentModalProvider; diff --git a/client/src/context/FocusElementsContext.tsx b/client/src/context/FocusElementsContext.tsx new file mode 100644 index 00000000..c6b22fb4 --- /dev/null +++ b/client/src/context/FocusElementsContext.tsx @@ -0,0 +1,58 @@ +import { createContext, FunctionComponent, useState } from 'react'; + +interface IFocusElementsContext { + uriBarView: any; + setUriBarView: any; + bodyEditorView: any; + setBodyEditorView: any; + requestScriptEditorView: any; + setRequestScriptEditorView: any; + responseScriptEditorView: any; + setResponseScriptEditorView: any; + i: number; + setI: any; +} + +const FocusElementsContext = createContext({ + uriBarView: null, + setUriBarView: () => {}, + bodyEditorView: null, + setBodyEditorView: () => {}, + requestScriptEditorView: null, + setRequestScriptEditorView: () => {}, + responseScriptEditorView: null, + setResponseScriptEditorView: () => {}, + i: 0, + setI: () => {}, +}); + +const FocusElementsProvider: FunctionComponent = ({ children }) => { + const [uriBarView, setUriBarView] = useState(); + const [bodyEditorView, setBodyEditorView] = useState(); + const [requestScriptEditorView, setRequestScriptEditorView] = useState(); + const [responseScriptEditorView, setResponseScriptEditorView] = useState(); + const [i, setI] = useState(0); + + return ( + + {children} + + ); +}; + +export { FocusElementsContext }; + +export default FocusElementsProvider; diff --git a/client/src/context/index.tsx b/client/src/context/index.tsx index 955f4db7..5af40a1d 100644 --- a/client/src/context/index.tsx +++ b/client/src/context/index.tsx @@ -1,9 +1,17 @@ import { FunctionComponent } from 'react'; +import CurrentModalProvider from './CurrentModalContext'; +import FocusElementsProvider from './FocusElementsContext'; import UserProvider, { UserContext } from './UserContext'; const ContextProvider: FunctionComponent = ({ children }) => { - return {children}; + return ( + + + {children} + + + ); }; export { UserContext }; diff --git a/client/src/index.css b/client/src/index.css index d115ec5d..a9093280 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -37,3 +37,8 @@ input:focus { ::-webkit-scrollbar-thumb:hover { background: #555; } + +/* NOTE: this makes sure that the cmd palette is in front of all other elements */ +.atom-overlay { + z-index: 9999999; +} diff --git a/client/src/pages/dashboard/Dashboard.tsx b/client/src/pages/dashboard/Dashboard.tsx index cdb5ce68..fe4ff13d 100644 --- a/client/src/pages/dashboard/Dashboard.tsx +++ b/client/src/pages/dashboard/Dashboard.tsx @@ -57,6 +57,11 @@ import { defaultCurrentRequest, } from '../../state/currentRequest'; import { CurrentScriptActionType, currentScriptReducer } from '../../state/currentScript'; +import { + defaultOpenHistory, + OpenHistoryActionType, + openHistoryReducer, +} from '../../state/openHistory'; import { BASE_PATH, errorToast, parseLocation, successToast } from '../../utils'; import styles from './Dashboard.module.css'; @@ -127,6 +132,10 @@ function Dashboard() { currentScriptReducer, undefined, ); + const [openHistory, dispatchOpenHistory] = useReducer( + openHistoryReducer, + defaultOpenHistory, + ); const [selectedRequestId, setSelectedRequestId] = useState( undefined, ); @@ -198,6 +207,10 @@ function Dashboard() { dispatchCurrentScript({ type: CurrentScriptActionType.UNSET, }); + dispatchOpenHistory({ + type: OpenHistoryActionType.PUSH, + item: { type: 'request', id: r.id }, + }); } openCollectionTree(collections, loc.collectionId); } else if (loc.collectionId && loc.scriptId) { @@ -213,6 +226,10 @@ function Dashboard() { dispatchCurrentCollection({ type: CurrentCollectionActionType.UNSET, }); + dispatchOpenHistory({ + type: OpenHistoryActionType.PUSH, + item: { type: 'script', id: s.id }, + }); } openCollectionTree(collections, loc.collectionId); } else if (loc.collectionId) { @@ -232,6 +249,10 @@ function Dashboard() { dispatchCurrentScript({ type: CurrentScriptActionType.UNSET, }); + dispatchOpenHistory({ + type: OpenHistoryActionType.PUSH, + item: { type: 'collection', id: c.id }, + }); } openCollectionTree(collections, loc.collectionId); } @@ -398,7 +419,7 @@ function Dashboard() { }; const dispatchSelectCollection = useCallback( - (id: number) => { + (id: number, pushHistory: boolean = true) => { const collection = findCollection(collections, id); if (!collection) throw new Error("Collection doesn't exist"); navigate(`/${id}`); @@ -412,12 +433,18 @@ function Dashboard() { dispatchCurrentScript({ type: CurrentScriptActionType.UNSET, }); + if (pushHistory) { + dispatchOpenHistory({ + type: OpenHistoryActionType.PUSH, + item: { type: 'collection', id: id }, + }); + } }, [collections, navigate], ); const dispatchSelectRequest = useCallback( - (id: number) => { + (id: number, pushHistory: boolean = true) => { const request = findRequest(collections, id); if (!request) throw new Error("Request doesn't exist"); navigate(`/${request.collectionId}/${request.id}`); @@ -431,12 +458,18 @@ function Dashboard() { dispatchCurrentScript({ type: CurrentScriptActionType.UNSET, }); + if (pushHistory) { + dispatchOpenHistory({ + type: OpenHistoryActionType.PUSH, + item: { type: 'request', id: id }, + }); + } }, [collections, navigate], ); const dispatchSelectScript = useCallback( - (id: number) => { + (id: number, pushHistory: boolean = true) => { const script = findScript(collections, id); if (!script) throw new Error(`Script ${id} doesn't exist`); navigate(`/${script.collectionId}/s-${script.id}`); @@ -450,6 +483,12 @@ function Dashboard() { type: CurrentScriptActionType.SET, script: script, }); + if (pushHistory) { + dispatchOpenHistory({ + type: OpenHistoryActionType.PUSH, + item: { type: 'script', id: id }, + }); + } }, [collections, navigate], ); @@ -500,7 +539,7 @@ function Dashboard() { ); const selectRequest = useCallback( - async (id: number) => { + async (id: number, pushHistory: boolean = true) => { try { if (currentRequest?.id === id) return; if (user?.data?.settings?.saveOnClose) { @@ -526,7 +565,7 @@ function Dashboard() { data: { ...currentScript.data }, }); } - dispatchSelectRequest(id); + dispatchSelectRequest(id, pushHistory); } else if (currentRequest?.isChanged && currentRequest?.id !== -1) { setSelectedRequestId(id); onSaveReqOpen(); @@ -537,8 +576,14 @@ function Dashboard() { setSelectedCollectionId(id); onSaveScriptOpen(); } else { - dispatchSelectRequest(id); + dispatchSelectRequest(id, pushHistory); } + const req = findRequest(collections, id); + if (!req) return; + dispatchCollections({ + type: CollectionsActionType.OPEN_COLLECTION_TREE, + id: req?.collectionId, + }); } catch (e) { console.error(e); errorToast('Could not select request', toast); @@ -549,6 +594,7 @@ function Dashboard() { user?.data?.settings?.saveOnClose, currentCollection, currentScript, + collections, dispatchSelectRequest, updateRequest, updateCollection, @@ -567,7 +613,7 @@ function Dashboard() { }, [selectRequest]); const selectCollection = useCallback( - async (id: number) => { + async (id: number, pushHistory: boolean = true) => { try { if (currentCollection?.id === id) return; if (user?.data?.settings?.saveOnClose) { @@ -593,7 +639,7 @@ function Dashboard() { data: { ...currentScript.data }, }); } - dispatchSelectCollection(id); + dispatchSelectCollection(id, pushHistory); } else if (currentRequest?.isChanged && currentRequest?.id !== -1) { setSelectedCollectionId(id); onSaveReqOpen(); @@ -604,8 +650,12 @@ function Dashboard() { setSelectedCollectionId(id); onSaveScriptOpen(); } else { - dispatchSelectCollection(id); + dispatchSelectCollection(id, pushHistory); } + dispatchCollections({ + type: CollectionsActionType.OPEN_COLLECTION_TREE, + id: id, + }); } catch (e) { console.error(e); errorToast('Could not select collection', toast); @@ -634,7 +684,7 @@ function Dashboard() { }, [selectCollection]); const selectScript = useCallback( - async (id: number) => { + async (id: number, pushHistory: boolean = true) => { try { if (currentScript?.id === id) return; if (user?.data?.settings?.saveOnClose) { @@ -660,7 +710,7 @@ function Dashboard() { data: { ...currentScript.data }, }); } - dispatchSelectScript(id); + dispatchSelectScript(id, pushHistory); } else if (currentRequest?.isChanged && currentRequest?.id !== -1) { setSelectedCollectionId(id); onSaveReqOpen(); @@ -671,14 +721,21 @@ function Dashboard() { setSelectedCollectionId(id); onSaveScriptOpen(); } else { - dispatchSelectScript(id); + dispatchSelectScript(id, pushHistory); } + const script = findScript(collections, id); + if (!script) return; + dispatchCollections({ + type: CollectionsActionType.OPEN_COLLECTION_TREE, + id: script?.collectionId, + }); } catch (e) { console.error(e); errorToast('Could not select script', toast); } }, [ + collections, currentCollection, currentRequest, currentScript, @@ -914,6 +971,8 @@ function Dashboard() { isExtInitialized={isExtInitialized} extVersion={extVersion} openExtModal={onOpen} + tabIndex={requestPanelTabIndex} + setTabIndex={setRequestPanelTabIndex} />
@@ -1101,13 +1160,19 @@ function Dashboard() {
Do you want to save the changes now? - {/* */} + setRequestPanelTabIndex={setRequestPanelTabIndex} + openHistory={openHistory} + dispatchOpenHistory={dispatchOpenHistory} + />
); } diff --git a/client/src/state/collections.tsx b/client/src/state/collections.tsx index 96c9cf76..c815fce9 100644 --- a/client/src/state/collections.tsx +++ b/client/src/state/collections.tsx @@ -19,6 +19,7 @@ enum CollectionsActionType { CHANGE_REQUEST_COLLECTION = 'CHANGE_REQUEST_COLLECTION', CLOSE_ALL = 'CLOSE_ALL', TOGGLE_OPEN_COLLECTION = 'TOGGLE_OPEN_COLLECTION', + OPEN_COLLECTION_TREE = 'OPEN_COLLECTION_TREE', SET_ENV_VAR = 'SET_ENV_VAR', PATCH_SCRIPT_DATA = 'PATCH_SCRIPT_DATA', MOVE_SCRIPT = 'MOVE_SCRIPT', @@ -87,6 +88,11 @@ type ToggleOpenCollectionAction = { id: number; }; +type OpenCollectionTreeAction = { + type: CollectionsActionType.OPEN_COLLECTION_TREE; + id: number; +}; + type SetEnvVarPayload = { collectionId: number; envName: string; @@ -456,6 +462,18 @@ function toggleOpenCollection(state: Collection[], id: number): Collection[] { }); } +function openCollectionTree(state: Collection[], id: number): Collection[] { + let c = findCollection(state, id); + while (c) { + c.open = true; + if (!c.data.parentId) { + break; + } + c = findCollection(state, c.data.parentId); + } + return [...state]; +} + function setEnvVar(state: Collection[], payload: SetEnvVarPayload): Collection[] { return modifyCollection(state, payload.collectionId, (c) => { const envs = c.data?.envs; @@ -561,6 +579,7 @@ type CollectionsAction = | MoveRequestAction | CloseAllAction | ToggleOpenCollectionAction + | OpenCollectionTreeAction | SetEnvVarAction | PatchScriptDataAction | MoveScriptAction @@ -595,6 +614,8 @@ function collectionsReducer( return closeAll(state); case CollectionsActionType.TOGGLE_OPEN_COLLECTION: return toggleOpenCollection(state, action.id); + case CollectionsActionType.OPEN_COLLECTION_TREE: + return openCollectionTree(state, action.id); case CollectionsActionType.SET_ENV_VAR: return setEnvVar(state, action.payload); case CollectionsActionType.PATCH_SCRIPT_DATA: diff --git a/client/src/state/openHistory.tsx b/client/src/state/openHistory.tsx new file mode 100644 index 00000000..06205a6f --- /dev/null +++ b/client/src/state/openHistory.tsx @@ -0,0 +1,92 @@ +type OpenHistoryItem = { + type: 'collection' | 'request' | 'script'; + id: number; +}; + +type OpenHistory = { + items: OpenHistoryItem[]; + index: number; +}; + +const defaultOpenHistory: OpenHistory = { + items: [], + index: -1, +}; + +enum OpenHistoryActionType { + PUSH = 'PUSH', + FORWARD = 'FORWARD', + BACKWARD = 'BACKWARD', +} + +type PushAction = { + type: OpenHistoryActionType.PUSH; + item: OpenHistoryItem; +}; + +type ForwardAction = { + type: OpenHistoryActionType.FORWARD; +}; + +type BackwardAction = { + type: OpenHistoryActionType.BACKWARD; +}; + +function push(state: OpenHistory, item: OpenHistoryItem): OpenHistory { + if (state.index === state.items.length - 1) { + return { + items: [...state.items, item], + index: state.index + 1, + }; + } + if ( + state.items[state.index + 1].id === item.id && + state.items[state.index + 1].type === item.type + ) { + return { + items: [...state.items], + index: state.index + 1, + }; + } + return { + items: [...state.items.slice(0, state.index + 1), item], + index: state.index + 1, + }; +} + +function forward(state: OpenHistory): OpenHistory { + return { + items: state.items, + index: Math.min(state.index + 1, state.items.length - 1), + }; +} + +function backward(state: OpenHistory): OpenHistory { + return { + items: state.items, + index: Math.max(state.index - 1, 0), + }; +} + +type OpenHistoryAction = PushAction | ForwardAction | BackwardAction; + +function openHistoryReducer( + state: OpenHistory | undefined = defaultOpenHistory, + action: OpenHistoryAction, +) { + switch (action.type) { + case OpenHistoryActionType.PUSH: + return push(state, action.item); + case OpenHistoryActionType.FORWARD: + return forward(state); + case OpenHistoryActionType.BACKWARD: + return backward(state); + default: + console.error('Invalid action type', action); + return state; + } +} + +export type { OpenHistory, OpenHistoryAction }; + +export { defaultOpenHistory, OpenHistoryActionType, openHistoryReducer }; diff --git a/server/src/main/kotlin/com/espero/yaade/server/routes/CollectionRoute.kt b/server/src/main/kotlin/com/espero/yaade/server/routes/CollectionRoute.kt index 72e8a905..96bc6845 100644 --- a/server/src/main/kotlin/com/espero/yaade/server/routes/CollectionRoute.kt +++ b/server/src/main/kotlin/com/espero/yaade/server/routes/CollectionRoute.kt @@ -94,6 +94,7 @@ class CollectionRoute(private val daoManager: DaoManager, private val vertx: Ver val data = ctx.body().asJsonObject() val userId = ctx.user().principal().getLong("id") val newCollection = CollectionDb(data, userId) + newCollection.createEnv("default", JsonObject().put("proxy", "server")) val parentId = newCollection.jsonData().getLong("parentId") if (parentId != null) {