From b2dcc38982a937b18bfdeb78289543be92aa6e0a Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Sun, 26 Mar 2023 12:02:20 -0700 Subject: [PATCH] Confirmation Dialogs --- src-web/components/App.tsx | 7 ++- src-web/components/DialogContext.tsx | 57 +++++++++++++++++++++++ src-web/components/ParameterEditor.tsx | 2 +- src-web/components/RequestPane.tsx | 2 +- src-web/components/Sidebar.tsx | 16 ++++--- src-web/components/Workspace.tsx | 1 + src-web/components/WorkspaceDropdown.tsx | 14 ++---- src-web/components/core/Button.tsx | 2 +- src-web/components/core/Dialog.tsx | 58 +++++++++++++++--------- src-web/hooks/Confirm.tsx | 16 +++++++ src-web/hooks/useConfirm.tsx | 14 ++++++ tailwind.config.cjs | 4 +- 12 files changed, 150 insertions(+), 43 deletions(-) create mode 100644 src-web/components/DialogContext.tsx create mode 100644 src-web/hooks/Confirm.tsx create mode 100644 src-web/hooks/useConfirm.tsx diff --git a/src-web/components/App.tsx b/src-web/components/App.tsx index 33ba6205..7793bdfe 100644 --- a/src-web/components/App.tsx +++ b/src-web/components/App.tsx @@ -20,6 +20,7 @@ import { DEFAULT_FONT_SIZE } from '../lib/constants'; import { extractKeyValue, getKeyValue, setKeyValue } from '../lib/keyValueStore'; import type { HttpRequest, HttpResponse, KeyValue, Workspace } from '../lib/models'; import { AppRouter } from './AppRouter'; +import { DialogProvider } from './DialogContext'; const queryClient = new QueryClient({ defaultOptions: { @@ -172,8 +173,10 @@ export function App() { - - + + + + diff --git a/src-web/components/DialogContext.tsx b/src-web/components/DialogContext.tsx new file mode 100644 index 00000000..8f9e09b6 --- /dev/null +++ b/src-web/components/DialogContext.tsx @@ -0,0 +1,57 @@ +import React, { createContext, useContext, useMemo, useState } from 'react'; +import type { DialogProps } from './core/Dialog'; +import { Dialog } from './core/Dialog'; + +type DialogEntry = { + id: string; + render: ({ hide }: { hide: () => void }) => React.ReactNode; +} & Pick; + +type DialogEntryOptionalId = Omit & { id?: string }; + +interface State { + dialogs: DialogEntry[]; + actions: Actions; +} + +interface Actions { + show: (d: DialogEntryOptionalId) => void; + hide: (id: string) => void; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const DialogContext = createContext({} as any); + +export const DialogProvider = ({ children }: { children: React.ReactNode }) => { + const [dialogs, setDialogs] = useState([]); + const actions = useMemo( + () => ({ + show: ({ id: oid, ...props }: DialogEntryOptionalId) => { + const id = oid ?? Math.random().toString(36).slice(2); + setDialogs((a) => [...a.filter((d) => d.id !== id), { id, ...props }]); + }, + hide: (id: string) => { + setDialogs((a) => a.filter((d) => d.id !== id)); + }, + }), + [], + ); + + const state: State = { + dialogs, + actions, + }; + + return ( + + {children} + {dialogs.map(({ id, render, ...props }) => ( + actions.hide(id)} {...props}> + {render({ hide: () => actions.hide(id) })} + + ))} + + ); +}; + +export const useDialog = () => useContext(DialogContext).actions; diff --git a/src-web/components/ParameterEditor.tsx b/src-web/components/ParameterEditor.tsx index 02feaff7..0a2aaac6 100644 --- a/src-web/components/ParameterEditor.tsx +++ b/src-web/components/ParameterEditor.tsx @@ -7,5 +7,5 @@ type Props = { }; export function ParametersEditor({ parameters, onChange }: Props) { - return ; + return ; } diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx index 134e1558..f805488a 100644 --- a/src-web/components/RequestPane.tsx +++ b/src-web/components/RequestPane.tsx @@ -66,7 +66,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN ], }, }, - { value: 'params', label: 'Params' }, + { value: 'params', label: 'URL Params' }, { value: 'headers', label: 'Headers' }, { value: 'auth', label: 'Auth' }, ], diff --git a/src-web/components/Sidebar.tsx b/src-web/components/Sidebar.tsx index 9965656c..6199d202 100644 --- a/src-web/components/Sidebar.tsx +++ b/src-web/components/Sidebar.tsx @@ -1,15 +1,16 @@ +import { dialog } from '@tauri-apps/api'; import classnames from 'classnames'; import type { ForwardedRef, KeyboardEvent } from 'react'; import React, { forwardRef, Fragment, memo, useCallback, useMemo, useRef, useState } from 'react'; import type { XYCoord } from 'react-dnd'; import { useDrag, useDrop } from 'react-dnd'; import { useActiveRequest } from '../hooks/useActiveRequest'; +import { useConfirm } from '../hooks/useConfirm'; import { useRequests } from '../hooks/useRequests'; import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest'; import { useUpdateRequest } from '../hooks/useUpdateRequest'; import type { HttpRequest } from '../lib/models'; import { Button } from './core/Button'; -import { Dialog } from './core/Dialog'; import { IconButton } from './core/IconButton'; import { HStack, VStack } from './core/Stacks'; import { DropMarker } from './DropMarker'; @@ -28,12 +29,12 @@ export const Sidebar = memo(function Sidebar({ className }: Props) { const sidebarRef = useRef(null); const unorderedRequests = useRequests(); const activeRequest = useActiveRequest(); + const confirm = useConfirm(); const requests = useMemo( () => [...unorderedRequests].sort((a, b) => a.sortPriority - b.sortPriority), [unorderedRequests], ); - const [open, setOpen] = useState(false); return (
- - Hello? - - setOpen(true)} /> + + confirm({ title: 'Reset Requests', description: 'Do you want to do it?' }) + } + />
diff --git a/src-web/components/Workspace.tsx b/src-web/components/Workspace.tsx index 322422c8..0e6a411c 100644 --- a/src-web/components/Workspace.tsx +++ b/src-web/components/Workspace.tsx @@ -31,6 +31,7 @@ export default function Workspace() { null, ); + // float/un-float sidebar on window resize useEffect(() => { if (windowSize.width <= WINDOW_FLOATING_SIDEBAR_WIDTH) { setFloating(true); diff --git a/src-web/components/WorkspaceDropdown.tsx b/src-web/components/WorkspaceDropdown.tsx index 3258118d..09ebe943 100644 --- a/src-web/components/WorkspaceDropdown.tsx +++ b/src-web/components/WorkspaceDropdown.tsx @@ -34,15 +34,6 @@ export const WorkspaceDropdown = memo(function WorkspaceDropdown({ className }: return [ ...workspaceItems, - { - type: 'separator', - label: activeWorkspace?.name, - }, - { - label: 'Delete', - leftSlot: , - onSelect: () => deleteWorkspace.mutate(), - }, { type: 'separator', label: 'Actions', @@ -52,6 +43,11 @@ export const WorkspaceDropdown = memo(function WorkspaceDropdown({ className }: leftSlot: , onSelect: () => createWorkspace.mutate({ name: 'New Workspace' }), }, + { + label: 'Delete Workspace', + leftSlot: , + onSelect: () => deleteWorkspace.mutate(), + }, ]; }, [workspaces, activeWorkspaceId]); diff --git a/src-web/components/core/Button.tsx b/src-web/components/core/Button.tsx index 13fc9c17..63378f56 100644 --- a/src-web/components/core/Button.tsx +++ b/src-web/components/core/Button.tsx @@ -7,7 +7,7 @@ import { Icon } from './Icon'; const colorStyles = { custom: '', default: 'text-gray-700 enabled:hover:bg-gray-700/10 enabled:hover:text-gray-1000', - gray: 'text-gray-800 bg-gray-100 enabled:hover:bg-gray-500/20 enabled:hover:text-gray-1000', + gray: 'text-gray-800 bg-highlight enabled:hover:bg-gray-500/20 enabled:hover:text-gray-1000', primary: 'bg-blue-400 text-white hover:bg-blue-500', secondary: 'bg-violet-400 text-white hover:bg-violet-500', warning: 'bg-orange-400 text-white hover:bg-orange-500', diff --git a/src-web/components/core/Dialog.tsx b/src-web/components/core/Dialog.tsx index 13e34d95..b41ee40f 100644 --- a/src-web/components/core/Dialog.tsx +++ b/src-web/components/core/Dialog.tsx @@ -1,18 +1,20 @@ import classnames from 'classnames'; import { motion } from 'framer-motion'; +import { useMemo } from 'react'; import type { ReactNode } from 'react'; import { Overlay } from '../Overlay'; import { IconButton } from './IconButton'; import { HStack, VStack } from './Stacks'; -interface Props { +export interface DialogProps { children: ReactNode; open: boolean; - onOpenChange: (open: boolean) => void; - title: string; - description?: string; + onClose: () => void; + title: ReactNode; + description?: ReactNode; className?: string; wide?: boolean; + hideX?: boolean; } export function Dialog({ @@ -20,38 +22,52 @@ export function Dialog({ className, wide, open, - onOpenChange, + onClose, title, description, -}: Props) { + hideX, +}: DialogProps) { + const titleId = useMemo(() => Math.random().toString(36).slice(2), []); + const descriptionId = useMemo( + () => (description ? Math.random().toString(36).slice(2) : undefined), + [description], + ); + return ( - onOpenChange(false)} portalName="dialog"> +
-
+
- onOpenChange(false)} - title="Close dialog" - aria-label="Close" - icon="x" - size="sm" - className="ml-auto absolute right-1 top-1" - /> + {!hideX && ( + + )} - -
{title}
-
- {description &&
{description}
} +

+ {title} +

+ {description &&

{description}

}
{children}
diff --git a/src-web/hooks/Confirm.tsx b/src-web/hooks/Confirm.tsx new file mode 100644 index 00000000..8a70b91f --- /dev/null +++ b/src-web/hooks/Confirm.tsx @@ -0,0 +1,16 @@ +import { Button } from '../components/core/Button'; +import { HStack } from '../components/core/Stacks'; + +interface Props { + hide: () => void; +} +export function Confirm({ hide }: Props) { + return ( + + + + + ); +} diff --git a/src-web/hooks/useConfirm.tsx b/src-web/hooks/useConfirm.tsx new file mode 100644 index 00000000..efb6da84 --- /dev/null +++ b/src-web/hooks/useConfirm.tsx @@ -0,0 +1,14 @@ +import { useDialog } from '../components/DialogContext'; +import { Confirm } from './Confirm'; + +export function useConfirm() { + const dialog = useDialog(); + return ({ title, description }: { title: string; description?: string }) => { + dialog.show({ + title, + description, + hideX: true, + render: Confirm, + }); + }; +} diff --git a/tailwind.config.cjs b/tailwind.config.cjs index 1bd38959..b68b0367 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -15,7 +15,7 @@ module.exports = { }, height: { "xs": "1.5rem", - "sm": "2.00rem", + "sm": "2.0rem", "md": "2.5rem" }, lineHeight: { @@ -23,7 +23,7 @@ module.exports = { "xs": "calc(1.5rem - 2px)", "sm": "calc(2.0rem - 2px)", "md": "calc(2.5rem - 2px)" - } + }, }, fontFamily: { "mono": ["JetBrains Mono", "Menlo", "monospace"],