Refactor editor to update better

This commit is contained in:
Gregory Schier
2023-03-10 10:39:23 -08:00
parent 43abb57f77
commit 5c96e83a22
15 changed files with 87 additions and 82 deletions

View File

@@ -1,5 +1,5 @@
import classnames from 'classnames';
import { useEffect, useState } from 'react';
import { useWindowSize } from 'react-use';
import { RequestPane } from './components/RequestPane';
import { ResponsePane } from './components/ResponsePane';
import { Sidebar } from './components/Sidebar';
@@ -12,16 +12,12 @@ type Params = {
requestId?: string;
};
export function App({ matches }: { path: string; matches?: Params }) {
export function Workspace({ matches }: { path: string; matches?: Params }) {
const workspaceId = matches?.workspaceId ?? '';
const { data: requests } = useRequests(workspaceId);
const request = requests?.find((r) => r.id === matches?.requestId);
const [screenWidth, setScreenWidth] = useState(window.innerWidth);
useEffect(() => {
window.addEventListener('resize', () => setScreenWidth(window.innerWidth));
}, []);
const isH = screenWidth > 900;
const { width } = useWindowSize();
const isH = width > 900;
return (
<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;
disabled?: boolean;
title?: string;
tabIndex?: number;
};
export const Button = forwardRef(function Button(
@@ -35,7 +36,6 @@ export const Button = forwardRef(function Button(
color,
justify = 'center',
size = 'md',
type = 'button',
...props
}: ButtonProps,
ref: ForwardedRef<HTMLButtonElement>,
@@ -43,7 +43,6 @@ export const Button = forwardRef(function Button(
return (
<button
ref={ref}
type={type}
className={classnames(
className,
'outline-none',

View File

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

View File

@@ -12,10 +12,6 @@
outline: none !important;
}
.cm-selectionBackground {
@apply bg-gray-300;
}
.cm-line {
@apply text-gray-900 pl-1 pr-1.5;
}
@@ -24,6 +20,15 @@
@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 {
@apply border-0 text-gray-500/60;

View File

@@ -1,15 +1,15 @@
import { defaultKeymap } from '@codemirror/commands';
import { useUnmount } from 'react-use';
import { Compartment, EditorState } from '@codemirror/state';
import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
import classnames from 'classnames';
import { EditorView } from 'codemirror';
import { useEffect, useState } from 'react';
import { useEffect, useRef } from 'react';
import './Editor.css';
import { useUnmount } from 'react-use';
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
import { singleLineExt } from './singleLine';
export interface EditorProps {
export interface _EditorProps {
id?: string;
readOnly?: boolean;
className?: string;
@@ -24,7 +24,7 @@ export interface EditorProps {
singleLine?: boolean;
}
export function Editor({
export function _Editor({
readOnly,
heightMode,
contentType,
@@ -35,16 +35,27 @@ export function Editor({
onChange,
className,
singleLine,
}: EditorProps) {
const [cm, setCm] = useState<{ view: EditorView; langHolder: Compartment } | null>(null);
const [divRef, setDivRef] = useState<HTMLDivElement | null>(null);
}: _EditorProps) {
console.log('ROUTERss');
const cm = useRef<{ view: EditorView; langHolder: Compartment } | null>(null);
// Unmount editor when component unmounts
useUnmount(() => cm?.view.destroy());
// Unmount the editor
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) => {
setDivRef(el);
if (divRef !== null || el === null) return;
if (el === null || cm.current !== null) return;
try {
const langHolder = new Compartment();
@@ -54,7 +65,7 @@ export function Editor({
extensions: [
langHolder.of(langExt),
...getExtensions({
container: divRef,
container: el,
readOnly,
placeholder,
singleLine,
@@ -64,28 +75,15 @@ export function Editor({
}),
],
});
let newView;
if (cm) {
newView = cm.view;
newView.setState(state);
} else {
newView = new EditorView({ state, parent: el });
}
setCm({ view: newView, langHolder });
const view = new EditorView({ state, parent: el });
cm.current = { view, langHolder };
syncGutterBg({ parent: el, className });
if (autoFocus && newView) newView.focus();
if (autoFocus) view.focus();
} catch (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 (
<div
ref={initDivRef}
@@ -109,7 +107,7 @@ function getExtensions({
contentType,
useTemplating,
}: Pick<
EditorProps,
_EditorProps,
'singleLine' | 'onChange' | 'contentType' | 'useTemplating' | 'placeholder' | 'readOnly'
> & { container: HTMLDivElement | null }) {
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
EditorView.updateListener.of((update) => {
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"
placeholder="name"
defaultValue={pair.header.name}
onChange={(name) =>
onChange({
id: pair.id,
header: { name },
})
}
onChange={(name) => onChange({ id: pair.id, header: { name } })}
/>
<Input
hideLabel
@@ -107,12 +102,7 @@ function FormRow({
useEditor={{ useTemplating: true }}
placeholder="value"
defaultValue={pair.header.value}
onChange={(value) =>
onChange({
id: pair.id,
header: { value },
})
}
onChange={(value) => onChange({ id: pair.id, header: { value } })}
/>
{onDelete && <IconButton icon="trash" onClick={() => onDelete(pair)} className="w-auto" />}
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,21 @@
import { useEffect, useState } from 'react';
import { useMount, useUnmount } from 'react-use';
import { ButtonLink } from '../components/ButtonLink';
import { Editor } from '../components/Editor';
import { Heading } from '../components/Heading';
import { VStack } from '../components/Stacks';
import { useWorkspaces } from '../hooks/useWorkspaces';
export function Workspaces(props: { path: string }) {
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 (
<VStack as="ul" className="p-12">
<Heading>Workspaces</Heading>
@@ -13,6 +24,7 @@ export function Workspaces(props: { path: string }) {
{w.name}
</ButtonLink>
))}
<Editor defaultValue={value} className="!bg-gray-50" onChange={setValue} />
</VStack>
);
}