From d90a7331c9ca70c1a806394f6eae2d92db20ba0d Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 15 Mar 2023 16:35:19 -0700 Subject: [PATCH] Add stuff to app header --- src-web/components/Workspace.tsx | 45 ++++++++++++++--- src-web/components/core/Button.tsx | 51 ++++++++------------ src-web/components/core/Editor/Editor.tsx | 21 ++++++-- src-web/components/core/IconButton.tsx | 33 +++++++++++-- src-web/components/editors/GraphQLEditor.tsx | 5 +- src-web/hooks/useTimedBoolean.ts | 19 ++++++++ 6 files changed, 126 insertions(+), 48 deletions(-) create mode 100644 src-web/hooks/useTimedBoolean.ts diff --git a/src-web/components/Workspace.tsx b/src-web/components/Workspace.tsx index 281ad0ae..af95fa40 100644 --- a/src-web/components/Workspace.tsx +++ b/src-web/components/Workspace.tsx @@ -1,27 +1,60 @@ import classnames from 'classnames'; +import { useNavigate } from 'react-router-dom'; import { useWindowSize } from 'react-use'; +import { useActiveRequest } from '../hooks/useActiveRequest'; +import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; +import { useWorkspaces } from '../hooks/useWorkspaces'; +import { Button } from './core/Button'; +import { DropdownMenuRadio, DropdownMenuTrigger } from './core/Dropdown'; +import { IconButton } from './core/IconButton'; +import { HStack } from './core/Stacks'; +import { WindowDragRegion } from './core/WindowDragRegion'; import { RequestPane } from './RequestPane'; import { ResponsePane } from './ResponsePane'; import { Sidebar } from './Sidebar'; -import { HStack } from './core/Stacks'; -import { WindowDragRegion } from './core/WindowDragRegion'; -import { useActiveRequest } from '../hooks/useActiveRequest'; export default function Workspace() { + const navigate = useNavigate(); const activeRequest = useActiveRequest(); + const activeWorkspace = useActiveWorkspace(); + const workspaces = useWorkspaces(); const { width } = useWindowSize(); const isSideBySide = width > 900; + if (activeWorkspace == null) { + return null; + } return (
-
+
- {activeRequest?.name} +
+ { + navigate(`/workspaces/${v.value}`); + }} + value={activeWorkspace?.id} + items={workspaces.map((w) => ({ label: w.name, value: w.id }))} + > + + + + +
+
+ {activeRequest?.name} +
+
+ +
(function Button( }: ButtonProps, ref, ) { + const classes = useMemo( + () => + classnames( + className, + 'outline-none pointer-events-auto', + 'border border-transparent focus-visible:border-blue-300', + 'rounded-md flex items-center', + colorStyles[color || 'default'], + justify === 'start' && 'justify-start', + justify === 'center' && 'justify-center', + size === 'md' && 'h-9 px-3', + size === 'sm' && 'h-7 px-2.5 text-sm', + ), + [color, size, justify, className], + ); + if (typeof to === 'string') { return ( - + {children} {forDropdown && } ); } else { return ( - diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index c71ca2f2..8e8d4a45 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -1,11 +1,11 @@ import { defaultKeymap } from '@codemirror/commands'; import { Compartment, EditorState } from '@codemirror/state'; -import { keymap, placeholder as placeholderExt, tooltips, ViewPlugin } from '@codemirror/view'; +import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view'; import classnames from 'classnames'; import { EditorView } from 'codemirror'; import type { MutableRefObject } from 'react'; import { useEffect, useMemo, useRef } from 'react'; -import { useMount, useUnmount } from 'react-use'; +import { useUnmount } from 'react-use'; import { IconButton } from '../IconButton'; import './Editor.css'; import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions'; @@ -67,7 +67,9 @@ export function _Editor({ const placeholderCompartment = useRef(new Compartment()); useEffect(() => { if (cm.current === null) return; - const effect = placeholderCompartment.current.reconfigure(placeholderExt(placeholder ?? '')); + const effect = placeholderCompartment.current.reconfigure( + placeholderExt(placeholderElFromText(placeholder ?? '')), + ); cm.current?.view.dispatch({ effects: effect }); }, [placeholder]); @@ -89,7 +91,9 @@ export function _Editor({ doc: `${defaultValue ?? ''}`, extensions: [ languageCompartment.of(langExt), - placeholderCompartment.current.of(placeholderExt(placeholder ?? '')), + placeholderCompartment.current.of( + placeholderExt(placeholderElFromText(placeholder ?? '')), + ), ...getExtensions({ container: wrapperRef.current, onChange: handleChange, @@ -138,8 +142,9 @@ export function _Editor({ {cmContainer} {format && ( { @@ -232,3 +237,9 @@ const syncGutterBg = ({ gutterEl?.classList.add(...bgClasses); } }; + +const placeholderElFromText = (text: string) => { + const el = document.createElement('div'); + el.innerHTML = text.replace('\n', '
'); + return el; +}; diff --git a/src-web/components/core/IconButton.tsx b/src-web/components/core/IconButton.tsx index f5eb62c6..6b46d3c0 100644 --- a/src-web/components/core/IconButton.tsx +++ b/src-web/components/core/IconButton.tsx @@ -1,20 +1,41 @@ import classnames from 'classnames'; import { forwardRef } from 'react'; +import { useTimedBoolean } from '../../hooks/useTimedBoolean'; import type { ButtonProps } from './Button'; import { Button } from './Button'; import type { IconProps } from './Icon'; import { Icon } from './Icon'; type Props = IconProps & - ButtonProps & { iconClassName?: string; iconSize?: IconProps['size']; title: string }; + ButtonProps & { + showConfirm?: boolean; + iconClassName?: string; + iconSize?: IconProps['size']; + title: string; + }; export const IconButton = forwardRef(function IconButton( - { icon, spin, className, iconClassName, size = 'md', iconSize, ...props }: Props, + { + showConfirm, + icon, + spin, + onClick, + className, + iconClassName, + size = 'md', + iconSize, + ...props + }: Props, ref, ) { + const [confirmed, setConfirmed] = useTimedBoolean(); return ( ); diff --git a/src-web/components/editors/GraphQLEditor.tsx b/src-web/components/editors/GraphQLEditor.tsx index 2d84dae5..11695350 100644 --- a/src-web/components/editors/GraphQLEditor.tsx +++ b/src-web/components/editors/GraphQLEditor.tsx @@ -4,7 +4,6 @@ import { useUniqueKey } from '../../hooks/useUniqueKey'; import { Divider } from '../core/Divider'; import type { EditorProps } from '../core/Editor'; import { Editor } from '../core/Editor'; -import { IconButton } from '../core/IconButton'; type Props = Pick; @@ -17,6 +16,9 @@ interface GraphQLBody { export function GraphQLEditor({ defaultValue, onChange, ...extraEditorProps }: Props) { const queryKey = useUniqueKey(); const { query, variables } = useMemo(() => { + if (!defaultValue) { + return { query: '', variables: {} }; + } try { const p = JSON.parse(defaultValue ?? '{}'); const query = p.query ?? ''; @@ -53,6 +55,7 @@ export function GraphQLEditor({ defaultValue, onChange, ...extraEditorProps }: P onChange={handleChangeQuery} contentType="application/graphql" format={formatSdl} + placeholder={`query { }`} {...extraEditorProps} /> diff --git a/src-web/hooks/useTimedBoolean.ts b/src-web/hooks/useTimedBoolean.ts new file mode 100644 index 00000000..2a52477f --- /dev/null +++ b/src-web/hooks/useTimedBoolean.ts @@ -0,0 +1,19 @@ +import { useRef, useState } from 'react'; +import { useUnmount } from 'react-use'; + +/** Returns a boolean that is true for a given number of milliseconds. */ +export function useTimedBoolean(millis = 1000): [boolean, () => void] { + const [value, setValue] = useState(false); + const timeout = useRef(null); + const reset = () => timeout.current && clearTimeout(timeout.current); + + useUnmount(reset); + + const setToTrue = () => { + setValue(true); + reset(); + timeout.current = setTimeout(() => setValue(false), millis); + }; + + return [value, setToTrue]; +}