Refactor editor to update better

This commit is contained in:
Gregory Schier
2023-03-10 10:39:23 -08:00
parent e9e3ba283c
commit 0b3497e5a1
15 changed files with 87 additions and 82 deletions

View File

@@ -1,5 +1,5 @@
import classnames from 'classnames'; import classnames from 'classnames';
import { useEffect, useState } from 'react'; import { useWindowSize } from 'react-use';
import { RequestPane } from './components/RequestPane'; import { RequestPane } from './components/RequestPane';
import { ResponsePane } from './components/ResponsePane'; import { ResponsePane } from './components/ResponsePane';
import { Sidebar } from './components/Sidebar'; import { Sidebar } from './components/Sidebar';
@@ -12,16 +12,12 @@ type Params = {
requestId?: string; requestId?: string;
}; };
export function App({ matches }: { path: string; matches?: Params }) { export function Workspace({ matches }: { path: string; matches?: Params }) {
const workspaceId = matches?.workspaceId ?? ''; const workspaceId = matches?.workspaceId ?? '';
const { data: requests } = useRequests(workspaceId); const { data: requests } = useRequests(workspaceId);
const request = requests?.find((r) => r.id === matches?.requestId); const request = requests?.find((r) => r.id === matches?.requestId);
const { width } = useWindowSize();
const [screenWidth, setScreenWidth] = useState(window.innerWidth); const isH = width > 900;
useEffect(() => {
window.addEventListener('resize', () => setScreenWidth(window.innerWidth));
}, []);
const isH = screenWidth > 900;
return ( return (
<div className="grid grid-cols-[auto_1fr] h-full text-gray-900 overflow-hidden"> <div className="grid grid-cols-[auto_1fr] h-full text-gray-900 overflow-hidden">

View File

@@ -0,0 +1,13 @@
import { Router } from 'preact-router';
import { Workspaces } from '../pages/Workspaces';
import { Workspace } from '../Workspace';
export function AppRouter() {
return (
<Router>
<Workspaces path="/" />
<Workspace path="/workspaces/:workspaceId" />
<Workspace path="/workspaces/:workspaceId/requests/:requestId" />
</Router>
);
}

View File

@@ -25,6 +25,7 @@ export type ButtonProps = {
children?: ComponentChildren; children?: ComponentChildren;
disabled?: boolean; disabled?: boolean;
title?: string; title?: string;
tabIndex?: number;
}; };
export const Button = forwardRef(function Button( export const Button = forwardRef(function Button(
@@ -35,7 +36,6 @@ export const Button = forwardRef(function Button(
color, color,
justify = 'center', justify = 'center',
size = 'md', size = 'md',
type = 'button',
...props ...props
}: ButtonProps, }: ButtonProps,
ref: ForwardedRef<HTMLButtonElement>, ref: ForwardedRef<HTMLButtonElement>,
@@ -43,7 +43,6 @@ export const Button = forwardRef(function Button(
return ( return (
<button <button
ref={ref} ref={ref}
type={type}
className={classnames( className={classnames(
className, className,
'outline-none', 'outline-none',

View File

@@ -11,7 +11,7 @@ export function ButtonLink({ href, className, ...buttonProps }: Props) {
const linkProps = { href }; const linkProps = { href };
return ( return (
<Link {...linkProps}> <Link {...linkProps}>
<Button className={classnames(className, 'w-full')} {...buttonProps} /> <Button className={classnames(className, 'w-full')} tabIndex={-1} {...buttonProps} />
</Link> </Link>
); );
} }

View File

@@ -12,10 +12,6 @@
outline: none !important; outline: none !important;
} }
.cm-selectionBackground {
@apply bg-gray-300;
}
.cm-line { .cm-line {
@apply text-gray-900 pl-1 pr-1.5; @apply text-gray-900 pl-1 pr-1.5;
} }
@@ -24,6 +20,15 @@
@apply text-placeholder; @apply text-placeholder;
} }
/* Don't show selection on blurred input */
.cm-selectionBackground {
@apply bg-transparent;
}
&.cm-focused .cm-selectionBackground {
@apply bg-gray-400;
}
/* Style gutters */
.cm-gutters { .cm-gutters {
@apply border-0 text-gray-500/60; @apply border-0 text-gray-500/60;

View File

@@ -1,15 +1,15 @@
import { defaultKeymap } from '@codemirror/commands'; import { defaultKeymap } from '@codemirror/commands';
import { useUnmount } from 'react-use';
import { Compartment, EditorState } from '@codemirror/state'; import { Compartment, EditorState } from '@codemirror/state';
import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view'; import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
import classnames from 'classnames'; import classnames from 'classnames';
import { EditorView } from 'codemirror'; import { EditorView } from 'codemirror';
import { useEffect, useState } from 'react'; import { useEffect, useRef } from 'react';
import './Editor.css'; import './Editor.css';
import { useUnmount } from 'react-use';
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions'; import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
import { singleLineExt } from './singleLine'; import { singleLineExt } from './singleLine';
export interface EditorProps { export interface _EditorProps {
id?: string; id?: string;
readOnly?: boolean; readOnly?: boolean;
className?: string; className?: string;
@@ -24,7 +24,7 @@ export interface EditorProps {
singleLine?: boolean; singleLine?: boolean;
} }
export function Editor({ export function _Editor({
readOnly, readOnly,
heightMode, heightMode,
contentType, contentType,
@@ -35,16 +35,27 @@ export function Editor({
onChange, onChange,
className, className,
singleLine, singleLine,
}: EditorProps) { }: _EditorProps) {
const [cm, setCm] = useState<{ view: EditorView; langHolder: Compartment } | null>(null); console.log('ROUTERss');
const [divRef, setDivRef] = useState<HTMLDivElement | null>(null); const cm = useRef<{ view: EditorView; langHolder: Compartment } | null>(null);
// Unmount editor when component unmounts // Unmount the editor
useUnmount(() => cm?.view.destroy()); useUnmount(() => {
cm.current?.view.destroy();
cm.current = null;
});
// Update language extension when contentType changes
useEffect(() => {
if (cm.current === null) return;
const { view, langHolder } = cm.current;
const ext = getLanguageExtension({ contentType, useTemplating });
view.dispatch({ effects: langHolder.reconfigure(ext) });
}, [contentType]);
// Initialize the editor
const initDivRef = (el: HTMLDivElement | null) => { const initDivRef = (el: HTMLDivElement | null) => {
setDivRef(el); if (el === null || cm.current !== null) return;
if (divRef !== null || el === null) return;
try { try {
const langHolder = new Compartment(); const langHolder = new Compartment();
@@ -54,7 +65,7 @@ export function Editor({
extensions: [ extensions: [
langHolder.of(langExt), langHolder.of(langExt),
...getExtensions({ ...getExtensions({
container: divRef, container: el,
readOnly, readOnly,
placeholder, placeholder,
singleLine, singleLine,
@@ -64,28 +75,15 @@ export function Editor({
}), }),
], ],
}); });
let newView; const view = new EditorView({ state, parent: el });
if (cm) { cm.current = { view, langHolder };
newView = cm.view;
newView.setState(state);
} else {
newView = new EditorView({ state, parent: el });
}
setCm({ view: newView, langHolder });
syncGutterBg({ parent: el, className }); syncGutterBg({ parent: el, className });
if (autoFocus && newView) newView.focus(); if (autoFocus) view.focus();
} catch (e) { } catch (e) {
console.log('Failed to initialize Codemirror', e); console.log('Failed to initialize Codemirror', e);
} }
}; };
// Update language extension when contentType changes
useEffect(() => {
if (cm === null) return;
const ext = getLanguageExtension({ contentType, useTemplating });
cm.view.dispatch({ effects: cm.langHolder.reconfigure(ext) });
}, [contentType]);
return ( return (
<div <div
ref={initDivRef} ref={initDivRef}
@@ -109,7 +107,7 @@ function getExtensions({
contentType, contentType,
useTemplating, useTemplating,
}: Pick< }: Pick<
EditorProps, _EditorProps,
'singleLine' | 'onChange' | 'contentType' | 'useTemplating' | 'placeholder' | 'readOnly' 'singleLine' | 'onChange' | 'contentType' | 'useTemplating' | 'placeholder' | 'readOnly'
> & { container: HTMLDivElement | null }) { > & { container: HTMLDivElement | null }) {
const ext = getLanguageExtension({ contentType, useTemplating }); const ext = getLanguageExtension({ contentType, useTemplating });
@@ -148,17 +146,6 @@ function getExtensions({
] ]
: []), : []),
// Clear selection on blur
EditorView.domEventHandlers({
blur: (e, view) => {
// Clear selection on blur. Must do on next tick or updating selection
// will keep the editor focused
setTimeout(() => {
view.dispatch({ selection: { anchor: 0, head: 0 } }, { userEvent: 'blur' });
});
},
}),
// Handle onChange // Handle onChange
EditorView.updateListener.of((update) => { EditorView.updateListener.of((update) => {
if (typeof onChange === 'function' && update.docChanged) { if (typeof onChange === 'function' && update.docChanged) {

View File

@@ -0,0 +1,6 @@
import { memo } from 'react';
import { _Editor } from './Editor';
import type { _EditorProps } from './Editor';
export type EditorProps = _EditorProps;
export const Editor = memo(_Editor);

View File

@@ -93,12 +93,7 @@ function FormRow({
label="Name" label="Name"
placeholder="name" placeholder="name"
defaultValue={pair.header.name} defaultValue={pair.header.name}
onChange={(name) => onChange={(name) => onChange({ id: pair.id, header: { name } })}
onChange({
id: pair.id,
header: { name },
})
}
/> />
<Input <Input
hideLabel hideLabel
@@ -107,12 +102,7 @@ function FormRow({
useEditor={{ useTemplating: true }} useEditor={{ useTemplating: true }}
placeholder="value" placeholder="value"
defaultValue={pair.header.value} defaultValue={pair.header.value}
onChange={(value) => onChange={(value) => onChange({ id: pair.id, header: { value } })}
onChange({
id: pair.id,
header: { value },
})
}
/> />
{onDelete && <IconButton icon="trash" onClick={() => onDelete(pair)} className="w-auto" />} {onDelete && <IconButton icon="trash" onClick={() => onDelete(pair)} className="w-auto" />}
</div> </div>

View File

@@ -1,7 +1,7 @@
import classnames from 'classnames'; import classnames from 'classnames';
import type { ComponentChildren } from 'preact'; import type { ComponentChildren } from 'preact';
import type { EditorProps } from './Editor/Editor'; import type { EditorProps } from './Editor';
import { Editor } from './Editor/Editor'; import { Editor } from './Editor';
import { HStack, VStack } from './Stacks'; import { HStack, VStack } from './Stacks';
interface Props { interface Props {

View File

@@ -1,7 +1,7 @@
import classnames from 'classnames'; import classnames from 'classnames';
import { useRequestUpdate, useSendRequest } from '../hooks/useRequest'; import { useRequestUpdate, useSendRequest } from '../hooks/useRequest';
import type { HttpRequest } from '../lib/models'; import type { HttpRequest } from '../lib/models';
import { Editor } from './Editor/Editor'; import { Editor } from './Editor';
import { HeaderEditor } from './HeaderEditor'; import { HeaderEditor } from './HeaderEditor';
import { TabContent, Tabs } from './Tabs'; import { TabContent, Tabs } from './Tabs';
import { UrlBar } from './UrlBar'; import { UrlBar } from './UrlBar';
@@ -27,9 +27,7 @@ export function RequestPane({ fullHeight, request, className }: Props) {
onUrlChange={(url) => updateRequest.mutate({ url })} onUrlChange={(url) => updateRequest.mutate({ url })}
sendRequest={sendRequest.mutate} sendRequest={sendRequest.mutate}
/> />
{/*<Divider />*/}
</div> </div>
{/*<Divider className="mb-2" />*/}
<Tabs <Tabs
tabs={[ tabs={[
{ value: 'body', label: 'JSON' }, { value: 'body', label: 'JSON' },

View File

@@ -2,7 +2,7 @@ import classnames from 'classnames';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useDeleteAllResponses, useDeleteResponse, useResponses } from '../hooks/useResponses'; import { useDeleteAllResponses, useDeleteResponse, useResponses } from '../hooks/useResponses';
import { Dropdown } from './Dropdown'; import { Dropdown } from './Dropdown';
import { Editor } from './Editor/Editor'; import { Editor } from './Editor';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
import { HStack } from './Stacks'; import { HStack } from './Stacks';

View File

@@ -1,7 +1,6 @@
import classnames from 'classnames'; import classnames from 'classnames';
import React, { useState } from 'react';
import { useRequestCreate } from '../hooks/useRequest'; import { useRequestCreate } from '../hooks/useRequest';
import useTheme from '../hooks/useTheme'; import { useTheme } from '../hooks/useTheme';
import type { HttpRequest } from '../lib/models'; import type { HttpRequest } from '../lib/models';
import { ButtonLink } from './ButtonLink'; import { ButtonLink } from './ButtonLink';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';

View File

@@ -10,7 +10,7 @@ import {
const appearanceQueryKey = ['theme', 'appearance']; const appearanceQueryKey = ['theme', 'appearance'];
export default function useTheme() { export function useTheme() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const appearance = useQuery({ const appearance = useQuery({
queryKey: appearanceQueryKey, queryKey: appearanceQueryKey,

View File

@@ -4,7 +4,8 @@ import { MotionConfig } from 'framer-motion';
import { render } from 'preact'; import { render } from 'preact';
import { Router } from 'preact-router'; import { Router } from 'preact-router';
import { HelmetProvider } from 'react-helmet-async'; import { HelmetProvider } from 'react-helmet-async';
import { App } from './App'; import { AppRouter } from './components/AppRouter';
import { Workspace } from './Workspace';
import { requestsQueryKey } from './hooks/useRequest'; import { requestsQueryKey } from './hooks/useRequest';
import { responsesQueryKey } from './hooks/useResponses'; import { responsesQueryKey } from './hooks/useResponses';
import { DEFAULT_FONT_SIZE } from './lib/constants'; import { DEFAULT_FONT_SIZE } from './lib/constants';
@@ -23,6 +24,7 @@ setAppearance();
const queryClient = new QueryClient(); const queryClient = new QueryClient();
await listen('updated_request', ({ payload: request }: { payload: HttpRequest }) => { await listen('updated_request', ({ payload: request }: { payload: HttpRequest }) => {
console.log('UPDATED REQUEST');
queryClient.setQueryData( queryClient.setQueryData(
requestsQueryKey(request.workspaceId), requestsQueryKey(request.workspaceId),
(requests: HttpRequest[] = []) => { (requests: HttpRequest[] = []) => {
@@ -45,12 +47,14 @@ await listen('updated_request', ({ payload: request }: { payload: HttpRequest })
}); });
await listen('deleted_request', ({ payload: request }: { payload: HttpRequest }) => { await listen('deleted_request', ({ payload: request }: { payload: HttpRequest }) => {
console.log('DELETED REQUEST');
queryClient.setQueryData(requestsQueryKey(request.workspaceId), (requests: HttpRequest[] = []) => queryClient.setQueryData(requestsQueryKey(request.workspaceId), (requests: HttpRequest[] = []) =>
requests.filter((r) => r.id !== request.id), requests.filter((r) => r.id !== request.id),
); );
}); });
await listen('updated_response', ({ payload: response }: { payload: HttpResponse }) => { await listen('updated_response', ({ payload: response }: { payload: HttpResponse }) => {
console.log('UPDATED RESPONSE');
queryClient.setQueryData( queryClient.setQueryData(
responsesQueryKey(response.requestId), responsesQueryKey(response.requestId),
(responses: HttpResponse[] = []) => { (responses: HttpResponse[] = []) => {
@@ -91,11 +95,7 @@ render(
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<MotionConfig transition={{ duration: 0.1 }}> <MotionConfig transition={{ duration: 0.1 }}>
<HelmetProvider> <HelmetProvider>
<Router> <AppRouter />
<Workspaces path="/" />
<App path="/workspaces/:workspaceId" />
<App path="/workspaces/:workspaceId/requests/:requestId" />
</Router>
</HelmetProvider> </HelmetProvider>
</MotionConfig> </MotionConfig>
</QueryClientProvider>, </QueryClientProvider>,

View File

@@ -1,10 +1,21 @@
import { useEffect, useState } from 'react';
import { useMount, useUnmount } from 'react-use';
import { ButtonLink } from '../components/ButtonLink'; import { ButtonLink } from '../components/ButtonLink';
import { Editor } from '../components/Editor';
import { Heading } from '../components/Heading'; import { Heading } from '../components/Heading';
import { VStack } from '../components/Stacks'; import { VStack } from '../components/Stacks';
import { useWorkspaces } from '../hooks/useWorkspaces'; import { useWorkspaces } from '../hooks/useWorkspaces';
export function Workspaces(props: { path: string }) { export function Workspaces(props: { path: string }) {
const workspaces = useWorkspaces(); const workspaces = useWorkspaces();
const [value, setValue] = useState<string>('hello wolrd');
useUnmount(() => {
console.log('UNMOUNT WORKSPACES');
});
useMount(() => {
console.log('MOUNT WORKSPACES');
});
console.log('RENDER WORKSPACES');
return ( return (
<VStack as="ul" className="p-12"> <VStack as="ul" className="p-12">
<Heading>Workspaces</Heading> <Heading>Workspaces</Heading>
@@ -13,6 +24,7 @@ export function Workspaces(props: { path: string }) {
{w.name} {w.name}
</ButtonLink> </ButtonLink>
))} ))}
<Editor defaultValue={value} className="!bg-gray-50" onChange={setValue} />
</VStack> </VStack>
); );
} }