Fixed the circular imports and things

This commit is contained in:
Gregory Schier
2024-12-20 23:49:15 -08:00
parent 51a11b6495
commit ec999015ab
83 changed files with 511 additions and 551 deletions

View File

@@ -1,3 +1,4 @@
import { useNavigate } from '@tanstack/react-router';
import classNames from 'classnames';
import { fuzzyFilter } from 'fuzzbunny';
import type { KeyboardEvent, ReactNode } from 'react';
@@ -11,6 +12,7 @@ import { useCreateHttpRequest } from '../hooks/useCreateHttpRequest';
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDebouncedState } from '../hooks/useDebouncedState';
import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useDialog } from '../hooks/useDialog';
import { useEnvironments } from '../hooks/useEnvironments';
import type { HotkeyAction } from '../hooks/useHotKey';
import { useHotKey } from '../hooks/useHotKey';
@@ -27,8 +29,6 @@ import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { router } from '../main';
import { Route } from '../routes/workspaces/$workspaceId/requests/$requestId';
import { CookieDialog } from './CookieDialog';
import { Button } from './core/Button';
import { Heading } from './core/Heading';
@@ -37,7 +37,6 @@ import { HttpMethodTag } from './core/HttpMethodTag';
import { Icon } from './core/Icon';
import { PlainInput } from './core/PlainInput';
import { HStack } from './core/Stacks';
import { useDialog } from './DialogContext';
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
interface CommandPaletteGroup {
@@ -78,6 +77,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
const deleteRequest = useDeleteRequest(activeRequest?.id ?? null);
const [, setSidebarHidden] = useSidebarHidden();
const openSettings = useOpenSettings();
const navigate = useNavigate();
const workspaceCommands = useMemo<CommandPaletteItem[]>(() => {
const commands: CommandPaletteItem[] = [
@@ -267,9 +267,9 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
<div className="truncate">{fallbackRequestName(r)}</div>
</HStack>
),
onSelect: () => {
router.navigate({
to: Route.fullPath,
onSelect: async () => {
await navigate({
to: '/workspaces/$workspaceId/requests/$requestId',
params: {
workspaceId: r.workspaceId,
requestId: r.id,
@@ -315,8 +315,9 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
}, [
workspaceCommands,
sortedRequests,
activeEnvironment?.id,
navigate,
sortedEnvironments,
activeEnvironment?.id,
setActiveEnvironmentId,
sortedWorkspaces,
openWorkspace,

View File

@@ -9,7 +9,7 @@ import { Dropdown, type DropdownItem } from './core/Dropdown';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { InlineCode } from './core/InlineCode';
import { useDialog } from './DialogContext';
import { useDialog } from '../hooks/useDialog';
export function CookieDropdown() {
const cookieJars = useCookieJars() ?? [];

View File

@@ -1,80 +1,5 @@
import React, { createContext, useContext, useMemo, useState } from 'react';
import { trackEvent } from '../lib/analytics';
import type { DialogProps } from './core/Dialog';
import { Dialog } from './core/Dialog';
type DialogEntry = {
id: string;
render: ({ hide }: { hide: () => void }) => React.ReactNode;
} & Omit<DialogProps, 'open' | 'children'>;
interface State {
dialogs: DialogEntry[];
actions: Actions;
}
interface Actions {
show: (d: DialogEntry) => void;
toggle: (d: DialogEntry) => void;
hide: (id: string) => void;
}
import { createContext } from 'react';
import type { DialogState } from './Dialogs';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const DialogContext = createContext<State>({} as State);
export const DialogProvider = ({ children }: { children: React.ReactNode }) => {
const [dialogs, setDialogs] = useState<State['dialogs']>([]);
const actions = useMemo<Actions>(
() => ({
show({ id, ...props }: DialogEntry) {
trackEvent('dialog', 'show', { id });
setDialogs((a) => [...a.filter((d) => d.id !== id), { id, ...props }]);
},
toggle({ id, ...props }: DialogEntry) {
if (dialogs.some((d) => d.id === id)) this.hide(id);
else this.show({ id, ...props });
},
hide: (id: string) => {
setDialogs((a) => a.filter((d) => d.id !== id));
},
}),
[dialogs],
);
const state: State = {
dialogs,
actions,
};
return <DialogContext.Provider value={state}>{children}</DialogContext.Provider>;
};
function DialogInstance({ id, render, onClose, ...props }: DialogEntry) {
const { actions } = useContext(DialogContext);
const children = render({ hide: () => actions.hide(id) });
return (
<Dialog
open
onClose={() => {
onClose?.();
actions.hide(id);
}}
{...props}
>
{children}
</Dialog>
);
}
export const useDialog = () => useContext(DialogContext).actions;
export function Dialogs() {
const { dialogs } = useContext(DialogContext);
return (
<>
{dialogs.map((props: DialogEntry) => (
<DialogInstance key={props.id} {...props} />
))}
</>
);
}
export const DialogContext = createContext<DialogState>({} as DialogState);

View File

@@ -0,0 +1,75 @@
import React, { useContext, useMemo, useState } from 'react';
import { trackEvent } from '../lib/analytics';
import { Dialog, type DialogProps } from './core/Dialog';
import { DialogContext } from './DialogContext';
type DialogEntry = {
id: string;
render: ({ hide }: { hide: () => void }) => React.ReactNode;
} & Omit<DialogProps, 'open' | 'children'>;
export interface DialogState {
dialogs: DialogEntry[];
actions: Actions;
}
interface Actions {
show: (d: DialogEntry) => void;
toggle: (d: DialogEntry) => void;
hide: (id: string) => void;
}
export const DialogProvider = ({ children }: { children: React.ReactNode }) => {
const [dialogs, setDialogs] = useState<DialogState['dialogs']>([]);
const actions = useMemo<Actions>(
() => ({
show({ id, ...props }: DialogEntry) {
trackEvent('dialog', 'show', { id });
setDialogs((a) => [...a.filter((d) => d.id !== id), { id, ...props }]);
},
toggle({ id, ...props }: DialogEntry) {
if (dialogs.some((d) => d.id === id)) this.hide(id);
else this.show({ id, ...props });
},
hide: (id: string) => {
setDialogs((a) => a.filter((d) => d.id !== id));
},
}),
[dialogs],
);
const state: DialogState = {
dialogs,
actions,
};
return <DialogContext.Provider value={state}>{children}</DialogContext.Provider>;
};
function DialogInstance({ id, render, onClose, ...props }: DialogEntry) {
const { actions } = useContext(DialogContext);
const children = render({ hide: () => actions.hide(id) });
return (
<Dialog
open
onClose={() => {
onClose?.();
actions.hide(id);
}}
{...props}
>
{children}
</Dialog>
);
}
export function Dialogs() {
const { dialogs } = useContext(DialogContext);
return (
<>
{dialogs.map((props: DialogEntry) => (
<DialogInstance key={props.id} {...props} />
))}
</>
);
}

View File

@@ -8,7 +8,7 @@ import { Button } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { useDialog } from './DialogContext';
import { useDialog } from '../hooks/useDialog';
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
type Props = {

View File

@@ -1,18 +1,20 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import { updateSchema } from 'cm6-graphql';
import type { EditorView } from 'codemirror';
import { formatSdl } from 'format-graphql';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useLocalStorage } from 'react-use';
import { useIntrospectGraphQL } from '../hooks/useIntrospectGraphQL';
import { tryFormatJson } from '../lib/formatters';
import { Button } from './core/Button';
import { Dropdown } from './core/Dropdown';
import type { EditorProps } from './core/Editor';
import { Editor, formatGraphQL } from './core/Editor';
import type { EditorProps } from './core/Editor/Editor';
import { Editor } from './core/Editor/Editor';
import { FormattedError } from './core/FormattedError';
import { Icon } from './core/Icon';
import { Separator } from './core/Separator';
import { useDialog } from './DialogContext';
import { useDialog } from '../hooks/useDialog';
type Props = Pick<EditorProps, 'heightMode' | 'className' | 'forceUpdateKey'> & {
baseRequest: HttpRequest;
@@ -168,7 +170,7 @@ export function GraphQLEditor({ body, onChange, baseRequest, ...extraEditorProps
<Editor
language="graphql"
heightMode="auto"
format={formatGraphQL}
format={formatSdl}
defaultValue={currentBody.query}
onChange={handleChangeQuery}
placeholder="..."

View File

@@ -16,13 +16,13 @@ import { tryFormatJson } from '../lib/formatters';
import type { GrpcRequest } from '@yaakapp-internal/models';
import { count } from '../lib/pluralize';
import { Button } from './core/Button';
import type { EditorProps } from './core/Editor';
import { Editor } from './core/Editor';
import { FormattedError } from './core/FormattedError';
import { InlineCode } from './core/InlineCode';
import { VStack } from './core/Stacks';
import { useDialog } from './DialogContext';
import { useDialog } from '../hooks/useDialog';
import { GrpcProtoSelection } from './GrpcProtoSelection';
import type { EditorProps} from './core/Editor/Editor';
import {Editor} from './core/Editor/Editor';
type Props = Pick<EditorProps, 'heightMode' | 'onChange' | 'className'> & {
services: ReflectResponseService[] | null;

View File

@@ -1,10 +1,11 @@
import classNames from 'classnames';
import type { HTMLAttributes, ReactNode } from 'react';
import React from 'react';
import { useSettings } from '../hooks/useSettings';
import { useOsInfo } from '../hooks/useOsInfo';
import { useSettings } from '../hooks/useSettings';
import { useStoplightsVisible } from '../hooks/useStoplightsVisible';
import { WINDOW_CONTROLS_WIDTH, WindowControls } from './WindowControls';
import { HEADER_SIZE_LG, HEADER_SIZE_MD, WINDOW_CONTROLS_WIDTH } from '../lib/constants';
import { WindowControls } from './WindowControls';
interface HeaderSizeProps extends HTMLAttributes<HTMLDivElement> {
children?: ReactNode;
@@ -13,9 +14,6 @@ interface HeaderSizeProps extends HTMLAttributes<HTMLDivElement> {
onlyXWindowControl?: boolean;
}
export const HEADER_SIZE_MD = '27px';
export const HEADER_SIZE_LG = '38px';
export function HeaderSize({
className,
style,
@@ -33,7 +31,8 @@ export function HeaderSize({
style={{
...style,
// Add padding for macOS stoplights, but keep it the same width (account for the interface scale)
paddingLeft: (stoplightsVisible && !ignoreControlsSpacing) ? 72 / settings.interfaceScale : undefined,
paddingLeft:
stoplightsVisible && !ignoreControlsSpacing ? 72 / settings.interfaceScale : undefined,
...(size === 'md' ? { height: HEADER_SIZE_MD } : {}),
...(size === 'lg' ? { height: HEADER_SIZE_LG } : {}),
...(osInfo.osType === 'macos' || ignoreControlsSpacing

View File

@@ -3,7 +3,7 @@ import { useLicense } from '@yaakapp-internal/license';
import { useOpenSettings } from '../hooks/useOpenSettings';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
import { SettingsTab } from './Settings/Settings';
import {SettingsTab} from "./Settings/SettingsTab";
const details: Record<
LicenseCheckStatus['type'],

View File

@@ -4,7 +4,7 @@ import { useRef } from 'react';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { useKeyValue } from '../hooks/useKeyValue';
import { Editor } from './core/Editor';
import {Editor} from "./core/Editor/Editor";
import { IconButton } from './core/IconButton';
import { SplitLayout } from './core/SplitLayout';
import { VStack } from './core/Stacks';

View File

@@ -1,16 +1,15 @@
import { useNavigate } from '@tanstack/react-router';
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
import React, { useState } from 'react';
import { useToast } from '../hooks/useToast';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { router } from '../main';
import { Route } from '../routes/workspaces/$workspaceId/index';
import { Button } from './core/Button';
import { InlineCode } from './core/InlineCode';
import { Select } from './core/Select';
import { VStack } from './core/Stacks';
import { useToast } from './ToastContext';
interface Props {
activeWorkspaceId: string;
@@ -23,6 +22,7 @@ export function MoveToWorkspaceDialog({ onDone, request, activeWorkspaceId }: Pr
const updateHttpRequest = useUpdateAnyHttpRequest();
const updateGrpcRequest = useUpdateAnyGrpcRequest();
const toast = useToast();
const navigate = useNavigate();
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string>(activeWorkspaceId);
return (
@@ -69,10 +69,10 @@ export function MoveToWorkspaceDialog({ onDone, request, activeWorkspaceId }: Pr
size="xs"
color="secondary"
className="mr-auto min-w-[5rem]"
onClick={() => {
onClick={async () => {
toast.hide('workspace-moved');
router.navigate({
to: Route.fullPath,
await navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: selectedWorkspaceId },
});
}}

View File

@@ -1,3 +1,4 @@
import { useNavigate } from '@tanstack/react-router';
import classNames from 'classnames';
import { useMemo, useRef } from 'react';
import { useKeyPressEvent } from 'react-use';
@@ -7,8 +8,6 @@ import { useHotKey } from '../hooks/useHotKey';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRequests } from '../hooks/useRequests';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { router } from '../main';
import { Route } from '../routes/workspaces/$workspaceId/requests/$requestId';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
import type { DropdownItem, DropdownRef } from './core/Dropdown';
@@ -22,6 +21,7 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
const allRecentRequestIds = useRecentRequests();
const recentRequestIds = useMemo(() => allRecentRequestIds.slice(1), [allRecentRequestIds]);
const requests = useRequests();
const navigate = useNavigate();
// Handle key-up
useKeyPressEvent('Control', undefined, () => {
@@ -52,9 +52,9 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
label: fallbackRequestName(request),
// leftSlot: <CountBadge className="!ml-0 px-0 w-5" count={recentRequestItems.length} />,
leftSlot: <HttpMethodTag className="text-right" shortNames request={request} />,
onSelect: () => {
router.navigate({
to: Route.fullPath,
onSelect: async () => {
await navigate({
to: '/workspaces/$workspaceId/requests/$requestId',
params: {
requestId: request.id,
workspaceId: activeWorkspace.id,
@@ -77,7 +77,7 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
}
return recentRequestItems.slice(0, 20);
}, [activeWorkspace, recentRequestIds, requests]);
}, [activeWorkspace, navigate, recentRequestIds, requests]);
return (
<Dropdown ref={dropdownRef} items={items}>

View File

@@ -1,16 +1,15 @@
import { useNavigate } from '@tanstack/react-router';
import { useEffect } from 'react';
import { getRecentCookieJars } from '../hooks/useRecentCookieJars';
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
import { getRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { router } from '../main';
import { Route as WorkspaceRoute } from '../routes/workspaces/$workspaceId';
import { Route as RequestRoute } from '../routes/workspaces/$workspaceId/requests/$requestId';
export function RedirectToLatestWorkspace() {
const workspaces = useWorkspaces();
const recentWorkspaces = useRecentWorkspaces();
const navigate = useNavigate();
useEffect(() => {
if (workspaces.length === 0) {
@@ -25,20 +24,20 @@ export function RedirectToLatestWorkspace() {
const requestId = (await getRecentRequests(workspaceId))[0] ?? null;
if (workspaceId != null && requestId != null) {
await router.navigate({
to: RequestRoute.fullPath,
await navigate({
to: '/workspaces/$workspaceId/requests/$requestId',
params: { workspaceId, requestId },
search: { cookieJarId, environmentId },
});
} else {
await router.navigate({
to: WorkspaceRoute.fullPath,
await navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId },
search: { cookieJarId, environmentId },
});
}
})();
}, [recentWorkspaces, workspaces, workspaces.length]);
}, [navigate, recentWorkspaces, workspaces, workspaces.length]);
return <></>;
}

View File

@@ -15,7 +15,7 @@ import { getHttpRequest } from '../lib/store';
import type { DropdownItem } from './core/Dropdown';
import { ContextMenu } from './core/Dropdown';
import { Icon } from './core/Icon';
import { useDialog } from './DialogContext';
import { useDialog } from '../hooks/useDialog';
import { FolderSettingsDialog } from './FolderSettingsDialog';
import type { SidebarTreeNode } from './Sidebar';

View File

@@ -13,6 +13,7 @@ import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEdit
import { useRequests } from '../hooks/useRequests';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { useToast } from '../hooks/useToast';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { languageFromContentType } from '../lib/contentType';
import { fallbackRequestName } from '../lib/fallbackRequestName';
@@ -34,7 +35,7 @@ import { BasicAuth } from './BasicAuth';
import { BearerAuth } from './BearerAuth';
import { BinaryFileEditor } from './BinaryFileEditor';
import { CountBadge } from './core/CountBadge';
import { Editor } from './core/Editor';
import { Editor } from './core/Editor/Editor';
import type {
GenericCompletionConfig,
GenericCompletionOption,
@@ -50,7 +51,6 @@ import { FormUrlencodedEditor } from './FormUrlencodedEditor';
import { GraphQLEditor } from './GraphQLEditor';
import { HeadersEditor } from './HeadersEditor';
import { MarkdownEditor } from './MarkdownEditor';
import { useToast } from './ToastContext';
import { UrlBar } from './UrlBar';
import { UrlParametersEditor } from './UrlParameterEditor';
@@ -446,25 +446,28 @@ export const RequestPane = memo(function RequestPane({
<EmptyStateText>Empty Body</EmptyStateText>
)}
</TabContent>
<TabContent value={TAB_DESCRIPTION}><div className="grid grid-rows-[auto_minmax(0,1fr)] h-full">
<PlainInput
label="Request Name"
hideLabel
defaultValue={activeRequest.name}
className="font-sans !text-xl !px-0"
containerClassName="border-0"
placeholder={fallbackRequestName(activeRequest)}
onChange={(name) => updateRequest.mutate({ id: activeRequestId, update: { name } })}
/>
<MarkdownEditor
name="request-description"
placeholder="A Markdown description of this request."
defaultValue={activeRequest.description}
onChange={(description) =>
updateRequest.mutate({ id: activeRequestId, update: { description } })
}
/>
</div>
<TabContent value={TAB_DESCRIPTION}>
<div className="grid grid-rows-[auto_minmax(0,1fr)] h-full">
<PlainInput
label="Request Name"
hideLabel
defaultValue={activeRequest.name}
className="font-sans !text-xl !px-0"
containerClassName="border-0"
placeholder={fallbackRequestName(activeRequest)}
onChange={(name) =>
updateRequest.mutate({ id: activeRequestId, update: { name } })
}
/>
<MarkdownEditor
name="request-description"
placeholder="A Markdown description of this request."
defaultValue={activeRequest.description}
onChange={(description) =>
updateRequest.mutate({ id: activeRequestId, update: { description } })
}
/>
</div>
</TabContent>
</Tabs>
</>

View File

@@ -1,12 +1,12 @@
import { useNavigate } from '@tanstack/react-router';
import { useRouteError } from 'react-router-dom';
import { router } from '../main';
import { Route } from '../routes/workspaces';
import { Button } from './core/Button';
import { FormattedError } from './core/FormattedError';
import { Heading } from './core/Heading';
import { VStack } from './core/Stacks';
export default function RouteError() {
const navigate = useNavigate();
const error = useRouteError();
console.log('Error', error);
const stringified = JSON.stringify(error);
@@ -20,8 +20,8 @@ export default function RouteError() {
<VStack space={2}>
<Button
color="primary"
onClick={() => {
router.navigate({ to: Route.fullPath });
onClick={async () => {
await navigate({ to: '/workspaces' });
}}
>
Go Home

View File

@@ -1,3 +1,4 @@
import { useSearch } from '@tanstack/react-router';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import classNames from 'classnames';
import React, { useState } from 'react';
@@ -12,18 +13,10 @@ import { SettingsGeneral } from './SettingsGeneral';
import { SettingsLicense } from './SettingsLicense';
import { SettingsPlugins } from './SettingsPlugins';
import { SettingsProxy } from './SettingsProxy';
import { SettingsTab } from './SettingsTab';
interface Props {
hide?: () => void;
defaultTab?: SettingsTab;
}
export enum SettingsTab {
General = 'general',
Proxy = 'proxy',
Appearance = 'appearance',
Plugins = 'plugins',
License = 'license',
}
const tabs = [
@@ -34,9 +27,10 @@ const tabs = [
SettingsTab.License,
];
export default function Settings({ hide, defaultTab }: Props) {
export default function Settings({ hide }: Props) {
const osInfo = useOsInfo();
const [tab, setTab] = useState<string>(defaultTab ?? SettingsTab.General);
const { tab: tabFromQuery } = useSearch({ from: '/workspaces/$workspaceId/settings' });
const [tab, setTab] = useState<string>(tabFromQuery ?? SettingsTab.General);
// Close settings window on escape
// TODO: Could this be put in a better place? Eg. in Rust key listener when creating the window

View File

@@ -9,7 +9,7 @@ import { getThemes } from '../../lib/theme/themes';
import { isThemeDark } from '../../lib/theme/window';
import type { ButtonProps } from '../core/Button';
import { Checkbox } from '../core/Checkbox';
import { Editor } from '../core/Editor';
import {Editor} from "../core/Editor/Editor";
import type { IconProps } from '../core/Icon';
import { Icon } from '../core/Icon';
import { IconButton } from '../core/IconButton';

View File

@@ -8,7 +8,7 @@ import { yaakDark } from '../../lib/theme/themes/yaak';
import { getThemeCSS } from '../../lib/theme/window';
import { Banner } from '../core/Banner';
import { Button } from '../core/Button';
import { Editor } from '../core/Editor';
import {Editor} from "../core/Editor/Editor";
import type { IconProps } from '../core/Icon';
import { Icon } from '../core/Icon';
import { IconButton } from '../core/IconButton';

View File

@@ -20,7 +20,7 @@ export function SettingsGeneral() {
const settings = useSettings();
const updateSettings = useUpdateSettings();
const appInfo = useAppInfo();
const checkForUpdates = useCheckForUpdates();
const checkForUpdates = useCheckForUpdates();
if (settings == null || workspace == null) {
return null;

View File

@@ -0,0 +1,8 @@
export enum SettingsTab {
General = 'general',
Proxy = 'proxy',
Appearance = 'appearance',
Plugins = 'plugins',
License = 'license',
}

View File

@@ -10,7 +10,7 @@ import type { DropdownRef } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { useDialog } from './DialogContext';
import { useDialog } from '../hooks/useDialog';
import { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
export function SettingsDropdown() {

View File

@@ -1,6 +1,7 @@
import { useNavigate } from '@tanstack/react-router';
import type { Folder, GrpcRequest, HttpRequest, Workspace } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { atom, useAtom } from 'jotai';
import { useAtom } from 'jotai';
import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
import { useKey, useKeyPressEvent } from 'react-use';
import { getActiveRequest } from '../hooks/useActiveRequest';
@@ -16,9 +17,8 @@ import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { router } from '../main';
import { Route } from '../routes/workspaces/$workspaceId/requests/$requestId';
import { ContextMenu } from './core/Dropdown';
import { sidebarSelectedIdAtom } from './SidebarAtoms';
import type { SidebarItemProps } from './SidebarItem';
import { SidebarItems } from './SidebarItems';
@@ -32,9 +32,6 @@ export interface SidebarTreeNode {
depth: number;
}
// This is an atom so we can use it in the child items to avoid re-rendering the entire list
export const sidebarSelectedIdAtom = atom<string | null>(null);
export const Sidebar = memo(function Sidebar({ className }: Props) {
const [hidden, setHidden] = useSidebarHidden();
const sidebarRef = useRef<HTMLLIElement>(null);
@@ -52,6 +49,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
const [draggingId, setDraggingId] = useState<string | null>(null);
const [hoveredTree, setHoveredTree] = useState<SidebarTreeNode | null>(null);
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const navigate = useNavigate();
const { value: collapsed, set: setCollapsed } = useKeyValue<Record<string, boolean>>({
key: ['sidebar_collapsed', activeWorkspace?.id ?? 'n/a'],
fallback: {},
@@ -165,8 +163,8 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
if (item.model === 'folder') {
await setCollapsed((c) => ({ ...c, [item.id]: !c[item.id] }));
} else {
router.navigate({
to: Route.fullPath,
await navigate({
to: '/workspaces/$workspaceId/requests/$requestId',
params: {
requestId: id,
workspaceId: item.workspaceId,
@@ -179,7 +177,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
setSelectedTree(tree);
}
},
[treeParentMap, setCollapsed, setHasFocus, setSelectedId],
[treeParentMap, setCollapsed, navigate, setSelectedId],
);
const handleClearSelected = useCallback(() => {
@@ -214,7 +212,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
);
});
useKeyPressEvent('Enter', (e) => {
useKeyPressEvent('Enter', async (e) => {
if (!hasFocus) return;
const selected = selectableRequests.find((r) => r.id === selectedId);
if (!selected || activeWorkspace == null) {
@@ -222,8 +220,8 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
}
e.preventDefault();
router.navigate({
to: Route.fullPath,
await navigate({
to: '/workspaces/$workspaceId/requests/$requestId',
params: {
requestId: selected.id,
workspaceId: activeWorkspace?.id ?? null,

View File

@@ -0,0 +1,5 @@
// This is an atom so we can use it in the child items to avoid re-rendering the entire list
import {atom} from "jotai/index";
export const sidebarSelectedIdAtom = atom<string | null>(null);

View File

@@ -8,14 +8,14 @@ import { activeRequestAtom } from '../hooks/useActiveRequest';
import { useScrollIntoView } from '../hooks/useScrollIntoView';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { jotaiStore } from '../lib/jotai';
import { isResponseLoading } from '../lib/model_util';
import { jotaiStore } from '../routes/__root';
import { HttpMethodTag } from './core/HttpMethodTag';
import { Icon } from './core/Icon';
import { StatusTag } from './core/StatusTag';
import { RequestContextMenu } from './RequestContextMenu';
import type { SidebarTreeNode } from './Sidebar';
import { sidebarSelectedIdAtom } from './Sidebar';
import {sidebarSelectedIdAtom} from "./SidebarAtoms";
import type { SidebarItemsProps } from './SidebarItems';
enum ItemTypes {
@@ -42,7 +42,7 @@ type DragItem = {
itemName: string;
};
function SidebarItem_({
export const SidebarItem = memo(function SidebarItem({
itemName,
itemId,
itemModel,
@@ -106,7 +106,7 @@ function SidebarItem_({
const [selected, setSelected] = useState<boolean>(
jotaiStore.get(sidebarSelectedIdAtom) == itemId,
);
useEffect(() => {
useEffect(() => {
jotaiStore.sub(sidebarSelectedIdAtom, () => {
const value = jotaiStore.get(sidebarSelectedIdAtom);
setSelected(value === itemId);
@@ -262,17 +262,4 @@ function SidebarItem_({
{children}
</li>
);
}
export const SidebarItem = memo<SidebarItemProps>(SidebarItem_, (a, b) => {
const different = [];
for (const key of Object.keys(a) as (keyof SidebarItemProps)[]) {
if (a[key] !== b[key]) {
different.push(key);
}
}
if (different.length > 0) {
console.log('ITEM DIFFERENT -------------------', different.join(', '));
}
return different.length === 0;
});

View File

@@ -23,7 +23,7 @@ export interface SidebarItemsProps {
grpcConnections: GrpcConnection[];
}
function SidebarItems_({
export const SidebarItems = memo(function SidebarItems({
tree,
selectedTree,
draggingId,
@@ -102,17 +102,4 @@ function SidebarItems_({
)}
</VStack>
);
}
export const SidebarItems = memo<SidebarItemsProps>(SidebarItems_, (a, b) => {
const different = [];
for (const key of Object.keys(a) as (keyof SidebarItemsProps)[]) {
if (a[key] !== b[key]) {
different.push(key);
}
}
if (different.length > 0) {
console.log('ITEMS DIFFERENT -------------------', different.join(', '));
}
return different.length === 0;
});
})

View File

@@ -1,105 +1,4 @@
import type { ShowToastRequest } from '@yaakapp-internal/plugin';
import { AnimatePresence } from 'framer-motion';
import type { ReactNode } from 'react';
import React, { createContext, useContext, useMemo, useRef, useState } from 'react';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { generateId } from '../lib/generateId';
import type { ToastProps } from './core/Toast';
import { Toast } from './core/Toast';
import { Portal } from './Portal';
import { createContext } from 'react';
import type { ToastState } from './Toasts';
type ToastEntry = {
id?: string;
message: ReactNode;
timeout?: 3000 | 5000 | 8000 | null;
onClose?: ToastProps['onClose'];
} & Omit<ToastProps, 'onClose' | 'open' | 'children' | 'timeout'>;
type PrivateToastEntry = ToastEntry & {
id: string;
timeout: number | null;
};
interface State {
toasts: PrivateToastEntry[];
actions: Actions;
}
interface Actions {
show: (d: ToastEntry) => void;
hide: (id: string) => void;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const ToastContext = createContext<State>({} as State);
export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
const [toasts, setToasts] = useState<State['toasts']>([]);
const timeoutRef = useRef<NodeJS.Timeout>();
const actions = useMemo<Actions>(
() => ({
show({ id, timeout = 5000, ...props }: ToastEntry) {
id = id ?? generateId();
if (timeout != null) {
timeoutRef.current = setTimeout(() => this.hide(id), timeout);
}
setToasts((a) => {
if (a.some((v) => v.id === id)) {
// It's already visible with this id
return a;
}
return [...a, { id, timeout, ...props }];
});
return id;
},
hide: (id: string) => {
setToasts((all) => {
const t = all.find((t) => t.id === id);
t?.onClose?.();
return all.filter((t) => t.id !== id);
});
},
}),
[],
);
useListenToTauriEvent<ShowToastRequest>('show_toast', (event) => {
actions.show({ ...event.payload });
});
const state: State = { toasts, actions };
return <ToastContext.Provider value={state}>{children}</ToastContext.Provider>;
};
function ToastInstance({ id, message, timeout, ...props }: PrivateToastEntry) {
const { actions } = useContext(ToastContext);
return (
<Toast
open
timeout={timeout}
{...props}
// We call onClose inside actions.hide instead of passing to toast so that
// it gets called from external close calls as well
onClose={() => actions.hide(id)}
>
{message}
</Toast>
);
}
export const useToast = () => useContext(ToastContext).actions;
export const Toasts = () => {
const { toasts } = useContext(ToastContext);
return (
<Portal name="toasts">
<div className="absolute right-0 bottom-0 z-20">
<AnimatePresence>
{toasts.map((props: PrivateToastEntry) => (
<ToastInstance key={props.id} {...props} />
))}
</AnimatePresence>
</div>
</Portal>
);
};
export const ToastContext = createContext<ToastState>({} as ToastState);

View File

@@ -0,0 +1,100 @@
import type { ShowToastRequest } from '@yaakapp-internal/plugin';
import { AnimatePresence } from 'framer-motion';
import React, {type ReactNode, useContext, useMemo, useRef, useState} from 'react';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { generateId } from '../lib/generateId';
import {Toast, type ToastProps} from './core/Toast';
import { Portal } from './Portal';
import { ToastContext } from './ToastContext';
type ToastEntry = {
id?: string;
message: ReactNode;
timeout?: 3000 | 5000 | 8000 | null;
onClose?: ToastProps['onClose'];
} & Omit<ToastProps, 'onClose' | 'open' | 'children' | 'timeout'>;
type PrivateToastEntry = ToastEntry & {
id: string;
timeout: number | null;
};
export interface ToastState {
toasts: PrivateToastEntry[];
actions: Actions;
}
export interface Actions {
show: (d: ToastEntry) => void;
hide: (id: string) => void;
}
export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
const [toasts, setToasts] = useState<ToastState['toasts']>([]);
const timeoutRef = useRef<NodeJS.Timeout>();
const actions = useMemo<Actions>(
() => ({
show({ id, timeout = 5000, ...props }: ToastEntry) {
id = id ?? generateId();
if (timeout != null) {
timeoutRef.current = setTimeout(() => this.hide(id), timeout);
}
setToasts((a) => {
if (a.some((v) => v.id === id)) {
// It's already visible with this id
return a;
}
return [...a, { id, timeout, ...props }];
});
return id;
},
hide: (id: string) => {
setToasts((all) => {
const t = all.find((t) => t.id === id);
t?.onClose?.();
return all.filter((t) => t.id !== id);
});
},
}),
[],
);
useListenToTauriEvent<ShowToastRequest>('show_toast', (event) => {
actions.show({ ...event.payload });
});
const state: ToastState = { toasts, actions };
return <ToastContext.Provider value={state}>{children}</ToastContext.Provider>;
};
function ToastInstance({ id, message, timeout, ...props }: PrivateToastEntry) {
const { actions } = useContext(ToastContext);
return (
<Toast
open
timeout={timeout}
{...props}
// We call onClose inside actions.hide instead of passing to toast so that
// it gets called from external close calls as well
onClose={() => actions.hide(id)}
>
{message}
</Toast>
);
}
export const Toasts = () => {
const { toasts } = useContext(ToastContext);
return (
<Portal name="toasts">
<div className="absolute right-0 bottom-0 z-20">
<AnimatePresence>
{toasts.map((props: PrivateToastEntry) => (
<ToastInstance key={props.id} {...props} />
))}
</AnimatePresence>
</div>
</Portal>
);
};

View File

@@ -2,6 +2,7 @@ import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import classNames from 'classnames';
import React, { useState } from 'react';
import { useOsInfo } from '../hooks/useOsInfo';
import {WINDOW_CONTROLS_WIDTH} from "../lib/constants";
import { Button } from './core/Button';
import { HStack } from './core/Stacks';
@@ -11,8 +12,6 @@ interface Props {
macos?: boolean;
}
export const WINDOW_CONTROLS_WIDTH = '10.5rem';
export function WindowControls({ className, onlyX }: Props) {
const [maximized, setMaximized] = useState<boolean>(false);
const osInfo = useOsInfo();

View File

@@ -1,7 +1,7 @@
import classNames from 'classnames';
import { motion } from 'framer-motion';
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useFloatingSidebarHidden } from '../hooks/useFloatingSidebarHidden';
@@ -41,10 +41,6 @@ export function Workspace() {
null,
);
useEffect(() => {
console.log('RENDER WORKSPACE');
}, []);
const unsub = () => {
if (moveState.current !== null) {
document.documentElement.removeEventListener('mousemove', moveState.current.move);

View File

@@ -14,7 +14,7 @@ import type { DropdownItem } from './core/Dropdown';
import { Icon } from './core/Icon';
import type { RadioDropdownItem } from './core/RadioDropdown';
import { RadioDropdown } from './core/RadioDropdown';
import { useDialog } from './DialogContext';
import { useDialog } from '../hooks/useDialog';
import { OpenWorkspaceDialog } from './OpenWorkspaceDialog';
import { WorkspaceSettingsDialog } from './WorkpaceSettingsDialog';

View File

@@ -1,5 +1,5 @@
import { useCallback, useMemo } from 'react';
import { Editor } from './Editor';
import {Editor} from "./Editor/Editor";
import type { PairEditorProps } from './PairEditor';
type Props = PairEditorProps;

View File

@@ -19,11 +19,11 @@ import {
useRef,
} from 'react';
import { useActiveEnvironmentVariables } from '../../../hooks/useActiveEnvironmentVariables';
import {useDialog} from "../../../hooks/useDialog";
import { parseTemplate } from '../../../hooks/useParseTemplate';
import { useRequestEditor } from '../../../hooks/useRequestEditor';
import { useSettings } from '../../../hooks/useSettings';
import { useTemplateFunctions } from '../../../hooks/useTemplateFunctions';
import { useDialog } from '../../DialogContext';
import { TemplateFunctionDialog } from '../../TemplateFunctionDialog';
import { TemplateVariableDialog } from '../../TemplateVariableDialog';
import { IconButton } from '../IconButton';
@@ -33,11 +33,6 @@ import { baseExtensions, getLanguageExtension, multiLineExtensions } from './ext
import type { GenericCompletionConfig } from './genericCompletion';
import { singleLineExt } from './singleLine';
// Export some things so all the code-split parts are in this file
export { buildClientSchema, getIntrospectionQuery } from 'graphql/utilities';
export { graphql } from 'cm6-graphql';
export { formatSdl } from 'format-graphql';
export interface EditorProps {
id?: string;
readOnly?: boolean;

View File

@@ -37,7 +37,7 @@ import type { EnvironmentVariable } from '@yaakapp-internal/models';
import type { TemplateFunction } from '@yaakapp-internal/plugin';
import { graphql } from 'cm6-graphql';
import { EditorView } from 'codemirror';
import type { EditorProps } from './index';
import type {EditorProps} from "./Editor";
import { pairs } from './pairs/extension';
import { text } from './text/extension';
import { twig } from './twig/extension';

View File

@@ -1,12 +0,0 @@
import * as editor from './Editor';
export type { EditorProps } from './Editor';
// TODO: Figure out why code-splitting breaks production build from
// showing any content
// const editor = await import('./Editor');
export const Editor = editor.Editor;
export const graphql = editor.graphql;
export const getIntrospectionQuery = editor.getIntrospectionQuery;
export const buildClientSchema = editor.buildClientSchema;
export const formatGraphQL = editor.formatSdl;

View File

@@ -3,8 +3,8 @@ import type { EditorView } from 'codemirror';
import type { HTMLAttributes, ReactNode } from 'react';
import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
import type { EditorProps } from './Editor';
import { Editor } from './Editor';
import type { EditorProps } from './Editor/Editor';
import { Editor } from './Editor/Editor';
import { IconButton } from './IconButton';
import { HStack } from './Stacks';

View File

@@ -8,8 +8,8 @@ import { useFormatText } from '../../hooks/useFormatText';
import { useResponseBodyEventSource } from '../../hooks/useResponseBodyEventSource';
import { isJSON } from '../../lib/contentType';
import { Button } from '../core/Button';
import type { EditorProps } from '../core/Editor';
import { Editor } from '../core/Editor';
import type { EditorProps } from '../core/Editor/Editor';
import { Editor } from '../core/Editor/Editor';
import { Icon } from '../core/Icon';
import { InlineCode } from '../core/InlineCode';
import { Separator } from '../core/Separator';

View File

@@ -10,14 +10,14 @@ import { useToggle } from '../../hooks/useToggle';
import { CopyButton } from '../CopyButton';
import { Banner } from '../core/Banner';
import { Button } from '../core/Button';
import type { EditorProps } from '../core/Editor';
import { Editor } from '../core/Editor';
import { hyperlink } from '../core/Editor/hyperlink/extension';
import { IconButton } from '../core/IconButton';
import { InlineCode } from '../core/InlineCode';
import { Input } from '../core/Input';
import { SizeTag } from '../core/SizeTag';
import { HStack } from '../core/Stacks';
import type { EditorProps } from '../core/Editor/Editor';
import { Editor } from '../core/Editor/Editor';
const extraExtensions = [hyperlink];
const LARGE_RESPONSE_BYTES = 2 * 1000 * 1000;