diff --git a/packages/insomnia-app/app/ui/components/design-empty-state.tsx b/packages/insomnia-app/app/ui/components/design-empty-state.tsx index eec5ce8ca1..7c6d260aaf 100644 --- a/packages/insomnia-app/app/ui/components/design-empty-state.tsx +++ b/packages/insomnia-app/app/ui/components/design-empty-state.tsx @@ -6,7 +6,6 @@ import styled from 'styled-components'; import { documentationLinks } from '../../common/documentation'; import { selectFileOrFolder } from '../../common/select-file-or-folder'; -import * as models from '../../models'; import { faint } from '../css/css-in-js'; import { selectActiveApiSpec } from '../redux/selectors'; import { showPrompt } from './modals'; @@ -41,25 +40,10 @@ interface Props { onUpdateContents: (contents: string) => void; } -const useUpdateApiSpecContents = () => { - const activeApiSpec = useSelector(selectActiveApiSpec); - return useCallback(async (contents: string) => { - if (!contents) { - return; - } - if (!activeApiSpec) { - return; - } - await models.apiSpec.update(activeApiSpec, { contents }); - }, [activeApiSpec]); -}; - const ImportSpecButton: FC = ({ onUpdateContents }) => { - const updateApiSpecContents = useUpdateApiSpecContents(); - const handleImportFile = useCallback(async () => { const { canceled, filePath } = await selectFileOrFolder({ - extensions: ['yml', 'yaml'], + extensions: ['yml', 'yaml', 'json'], itemTypes: ['file'], }); // Exit if no file selected @@ -68,8 +52,8 @@ const ImportSpecButton: FC = ({ onUpdateContents }) => { } const contents = String(await fs.promises.readFile(filePath)); - await updateApiSpecContents(contents); - }, [updateApiSpecContents]); + onUpdateContents(contents); + }, [onUpdateContents]); const handleImportUri = useCallback(async () => { showPrompt({ @@ -83,15 +67,14 @@ const ImportSpecButton: FC = ({ onUpdateContents }) => { return; } const contents = await response.text(); - await updateApiSpecContents(contents); onUpdateContents(contents); }, }); - }, [updateApiSpecContents, onUpdateContents]); + }, [onUpdateContents]); const button = ( - Import + Import OpenAPI ); @@ -117,21 +100,19 @@ const ImportSpecButton: FC = ({ onUpdateContents }) => { const SecondaryAction: FC = ({ onUpdateContents }) => { const PETSTORE_EXAMPLE_URI = 'https://gist.githubusercontent.com/gschier/4e2278d5a50b4bbf1110755d9b48a9f9/raw/801c05266ae102bcb9288ab92c60f52d45557425/petstore-spec.yaml'; - const updateApiSpecContents = useUpdateApiSpecContents(); const onClick = useCallback(async () => { const response = await window.fetch(PETSTORE_EXAMPLE_URI); if (!response) { return; } const contents = await response.text(); - await updateApiSpecContents(contents); onUpdateContents(contents); - }, [updateApiSpecContents, onUpdateContents]); + }, [onUpdateContents]); return (
- Or import existing an OpenAPI spec or start from an example + Or import an existing OpenAPI spec or start from an example
diff --git a/packages/insomnia-app/app/ui/components/wrapper-design.tsx b/packages/insomnia-app/app/ui/components/wrapper-design.tsx index 42df791879..3a79728ee8 100644 --- a/packages/insomnia-app/app/ui/components/wrapper-design.tsx +++ b/packages/insomnia-app/app/ui/components/wrapper-design.tsx @@ -1,12 +1,13 @@ import { IRuleResult } from '@stoplight/spectral'; import { Button, Notice, NoticeTable } from 'insomnia-components'; -import React, { createRef, FC, Fragment, ReactNode, RefObject, useCallback, useState } from 'react'; +import React, { createRef, FC, Fragment, ReactNode, RefObject, useCallback, useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; -import { useAsync, useDebounce } from 'react-use'; +import { useAsync } from 'react-use'; import styled from 'styled-components'; import SwaggerUI from 'swagger-ui-react'; import { parseApiSpec, ParsedApiSpec } from '../../common/api-specs'; +import { debounce } from '../../common/misc'; import { initializeSpectral, isLintError } from '../../common/spectral'; import * as models from '../../models/index'; import { superFaint } from '../css/css-in-js'; @@ -74,24 +75,65 @@ interface LintMessage { range: IRuleResult['range']; } -const RenderEditor: FC<{ editor: RefObject }> = ({ editor }) => { +const useDesignEmptyState = () => { const activeApiSpec = useSelector(selectActiveApiSpec); + const contents = activeApiSpec?.contents; + const [forceRefreshCounter, setForceRefreshCounter] = useState(0); - const [lintMessages, setLintMessages] = useState([]); - const contents = activeApiSpec?.contents ?? ''; - const [contentsState, setContentsState] = useState(contents); + const [shouldIncrementCounter, setShouldIncrementCounter] = useState(false); - const uniquenessKey = `${forceRefreshCounter}::${activeApiSpec?._id}`; + useEffect(() => { + if (contents && shouldIncrementCounter){ + setForceRefreshCounter(forceRefreshCounter => forceRefreshCounter + 1); + setShouldIncrementCounter(false); + } + }, [contents, shouldIncrementCounter]); - useDebounce(async () => { + const onUpdateContents = useCallback((value: string) => { if (!activeApiSpec) { return; } - await models.apiSpec.update({ ...activeApiSpec, contents: contentsState }); - setForceRefreshCounter(forceRefreshCounter => forceRefreshCounter + 1); - // eslint-disable-next-line react-hooks/exhaustive-deps -- this is a problem with react-use - }, 500, [contentsState]); + const fn = async () => { + await models.apiSpec.update({ ...activeApiSpec, contents: value }); + }; + fn(); + + // Because we can't await activeApiSpec.contents to have propageted to redux, we flip a toggle to decide if we should do something when redux does eventually change + setShouldIncrementCounter(true); + }, [activeApiSpec]); + + const emptyStateNode = contents ? null : ( + + ); + + const uniquenessKey = `${forceRefreshCounter}::${activeApiSpec?._id}`; + return { uniquenessKey, emptyStateNode }; +}; + +const RenderEditor: FC<{ editor: RefObject }> = ({ editor }) => { + const activeApiSpec = useSelector(selectActiveApiSpec); + const [lintMessages, setLintMessages] = useState([]); + const contents = activeApiSpec?.contents ?? ''; + + const { uniquenessKey, emptyStateNode } = useDesignEmptyState(); + + const onCodeEditorChange = useMemo(() => { + const handler = (contents: string) => { + const fn = async () => { + if (!activeApiSpec) { + return; + } + + await models.apiSpec.update({ ...activeApiSpec, contents }); + }; + fn(); + }; + + return debounce(handler, 500); + }, [activeApiSpec]); useAsync(async () => { // Lint only if spec has content @@ -136,13 +178,11 @@ const RenderEditor: FC<{ editor: RefObject }> = ({ editor ref={editor} lintOptions={{ delay: 1000 }} mode="openapi" - defaultValue={contentsState} - onChange={setContentsState} + defaultValue={contents} + onChange={onCodeEditorChange} uniquenessKey={uniquenessKey} /> - + {emptyStateNode} {lintMessages.length > 0 && (