Switch to BiomeJS (#306)

This commit is contained in:
Gregory Schier
2025-11-23 08:38:13 -08:00
committed by GitHub
parent 2bac610efe
commit ec3e2e16a9
332 changed files with 3007 additions and 4097 deletions

View File

@@ -1,6 +1,6 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import mime from 'mime';
import { useKeyValue } from '../hooks/useKeyValue';
import type { HttpRequest } from '@yaakapp-internal/models';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { InlineCode } from './core/InlineCode';

View File

@@ -14,7 +14,6 @@ const featureMap: Record<Props['feature'], boolean> = {
export function CargoFeature({ children, feature }: Props) {
if (featureMap[feature]) {
return <>{children}</>;
} else {
return null;
}
return null;
}

View File

@@ -17,12 +17,12 @@ export function ColorIndicator({ color, onClick, className }: Props) {
if (onClick) {
return (
<button
type="button"
onClick={onClick}
style={style}
className={classNames(finalClassName, 'hover:border-text')}
/>
);
} else {
return <span style={style} className={finalClassName} />;
}
return <span style={style} className={finalClassName} />;
}

View File

@@ -2,8 +2,15 @@ import { workspacesAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { fuzzyFilter } from 'fuzzbunny';
import { useAtomValue } from 'jotai';
import type { KeyboardEvent, ReactNode } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Fragment,
type KeyboardEvent,
type ReactNode,
useCallback,
useMemo,
useRef,
useState,
} from 'react';
import { createFolder } from '../commands/commands';
import { createSubEnvironmentAndActivate } from '../commands/createEnvironment';
import { openSettings } from '../commands/openSettings';
@@ -77,6 +84,11 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
const [, setSidebarHidden] = useSidebarHidden();
const { mutate: sendRequest } = useSendAnyHttpRequest();
const handleSetCommand = (command: string) => {
setCommand(command);
setSelectedItemKey(null);
};
const workspaceCommands = useMemo<CommandPaletteItem[]>(() => {
if (workspaceId == null) return [];
@@ -150,25 +162,23 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
label: 'Send Request',
onSelect: () => sendRequest(activeRequest.id),
});
for (let i = 0; i < httpRequestActions.length; i++) {
const a = httpRequestActions[i]!;
httpRequestActions.forEach((a, i) => {
commands.push({
key: `http_request_action.${i}`,
label: a.label,
onSelect: () => a.call(activeRequest),
});
}
});
}
if (activeRequest?.model === 'grpc_request') {
for (let i = 0; i < grpcRequestActions.length; i++) {
const a = grpcRequestActions[i]!;
grpcRequestActions.forEach((a, i) => {
commands.push({
key: `grpc_request_action.${i}`,
label: a.label,
onSelect: () => a.call(activeRequest),
});
}
});
}
if (activeRequest != null) {
@@ -210,13 +220,14 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
if (aRecentIndex >= 0 && bRecentIndex >= 0) {
return aRecentIndex - bRecentIndex;
} else if (aRecentIndex >= 0 && bRecentIndex === -1) {
return -1;
} else if (aRecentIndex === -1 && bRecentIndex >= 0) {
return 1;
} else {
return a.createdAt.localeCompare(b.createdAt);
}
if (aRecentIndex >= 0 && bRecentIndex === -1) {
return -1;
}
if (aRecentIndex === -1 && bRecentIndex >= 0) {
return 1;
}
return a.createdAt.localeCompare(b.createdAt);
});
}, [recentRequests, requests]);
@@ -227,13 +238,14 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
if (aRecentIndex >= 0 && bRecentIndex >= 0) {
return aRecentIndex - bRecentIndex;
} else if (aRecentIndex >= 0 && bRecentIndex === -1) {
return -1;
} else if (aRecentIndex === -1 && bRecentIndex >= 0) {
return 1;
} else {
return a.createdAt.localeCompare(b.createdAt);
}
if (aRecentIndex >= 0 && bRecentIndex === -1) {
return -1;
}
if (aRecentIndex === -1 && bRecentIndex >= 0) {
return 1;
}
return a.createdAt.localeCompare(b.createdAt);
});
}, [subEnvironments, recentEnvironments]);
@@ -249,13 +261,14 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
if (aRecentIndex >= 0 && bRecentIndex >= 0) {
return aRecentIndex - bRecentIndex;
} else if (aRecentIndex >= 0 && bRecentIndex === -1) {
return -1;
} else if (aRecentIndex === -1 && bRecentIndex >= 0) {
return 1;
} else {
return a.createdAt.localeCompare(b.createdAt);
}
if (aRecentIndex >= 0 && bRecentIndex === -1) {
return -1;
}
if (aRecentIndex === -1 && bRecentIndex >= 0) {
return 1;
}
return a.createdAt.localeCompare(b.createdAt);
});
}, [recentWorkspaces, workspaces]);
@@ -280,12 +293,10 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
<div className="flex items-center gap-x-0.5">
<HttpMethodTag short className="text-xs mr-2" request={r} />
{resolvedModelNameWithFoldersArray(r).map((name, i, all) => (
<>
{i !== 0 && (
<Icon icon="chevron_right" className="opacity-80"/>
)}
<Fragment key={name}>
{i !== 0 && <Icon icon="chevron_right" className="opacity-80" />}
<div className={classNames(i < all.length - 1 && 'truncate')}>{name}</div>
</>
</Fragment>
))}
</div>
),
@@ -341,10 +352,6 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
const allItems = useMemo(() => groups.flatMap((g) => g.items), [groups]);
useEffect(() => {
setSelectedItemKey(null);
}, [command]);
const { filteredGroups, filteredAllItems } = useMemo(() => {
const result = command
? fuzzyFilter(
@@ -424,7 +431,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
placeholder="Search or type a command"
className="font-sans !text-base"
defaultValue={command}
onChange={setCommand}
onChange={handleSetCommand}
onKeyDownCapture={handleKeyDown}
/>
</div>

View File

@@ -1,6 +1,6 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import { patchModel } from '@yaakapp-internal/models';
import { type ReactNode } from 'react';
import type { ReactNode } from 'react';
import { useToggle } from '../hooks/useToggle';
import { showConfirm } from '../lib/confirm';
import { Banner } from './core/Banner';
@@ -54,7 +54,7 @@ export function ConfirmLargeRequestBody({ children, request }: Props) {
variant="border"
onClick={async () => {
const confirm = await showConfirm({
id: 'delete-body-' + request.id,
id: `delete-body-${request.id}`,
confirmText: 'Delete Body',
title: 'Delete Body Text',
description: 'Are you sure you want to delete the request body text?',

View File

@@ -1,4 +1,4 @@
import type { Cookie} from '@yaakapp-internal/models';
import type { Cookie } from '@yaakapp-internal/models';
import { cookieJarsAtom, patchModel } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { cookieDomain } from '../lib/model_util';
@@ -10,7 +10,7 @@ interface Props {
cookieJarId: string | null;
}
export const CookieDialog = function ({ cookieJarId }: Props) {
export const CookieDialog = ({ cookieJarId }: Props) => {
const cookieJars = useAtomValue(cookieJarsAtom);
const cookieJar = cookieJars?.find((c) => c.id === cookieJarId);
@@ -33,7 +33,7 @@ export const CookieDialog = function ({ cookieJarId }: Props) {
<tr>
<th className="py-2 text-left">Domain</th>
<th className="py-2 text-left pl-4">Cookie</th>
<th className="py-2 pl-4"></th>
<th className="py-2 pl-4" />
</tr>
</thead>
<tbody className="divide-y divide-surface-highlight">

View File

@@ -1,6 +1,6 @@
import { useAtomValue } from 'jotai';
import type { ComponentType } from 'react';
import React, { useCallback } from 'react';
import { useCallback } from 'react';
import { dialogsAtom, hideDialog } from '../lib/dialog';
import { Dialog, type DialogProps } from './core/Dialog';
import { ErrorBoundary } from './ErrorBoundary';

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames';
import type { CSSProperties} from 'react';
import React, { memo } from 'react';
import type { CSSProperties } from 'react';
import { memo } from 'react';
interface Props {
className?: string;

View File

@@ -32,7 +32,6 @@ import { VStack } from './core/Stacks';
import { Markdown } from './Markdown';
import { SelectFile } from './SelectFile';
// eslint-disable-next-line react-refresh/only-export-components
export const DYNAMIC_FORM_NULL_ARG = '__NULL__';
const INPUT_SIZE = 'sm';
@@ -59,7 +58,7 @@ export function DynamicForm<T extends Record<string, JsonPrimitive>>({
}: Props<T>) {
const setDataAttr = useCallback(
(name: string, value: JsonPrimitive) => {
onChange({ ...data, [name]: value == DYNAMIC_FORM_NULL_ARG ? undefined : value });
onChange({ ...data, [name]: value === DYNAMIC_FORM_NULL_ARG ? undefined : value });
},
[data, onChange],
);
@@ -128,7 +127,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
case 'select':
return (
<SelectArg
key={i}
key={i + stateKey}
arg={input}
onChange={(v) => setDataAttr(input.name, v)}
value={
@@ -250,6 +249,9 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
);
case 'markdown':
return <Markdown key={i + stateKey}>{input.content}</Markdown>;
default:
// @ts-expect-error
throw new Error(`Invalid input type: ${input.type}`);
}
})}
</>
@@ -293,9 +295,8 @@ function TextArg({
};
if (autocompleteVariables || autocompleteFunctions) {
return <Input {...props} />;
} else {
return <PlainInput {...props} />;
}
return <PlainInput {...props} />;
}
function EditorArg({
@@ -491,7 +492,7 @@ function HttpRequestArg({
return {
label:
buildRequestBreadcrumbs(r, folders).join(' / ') +
(r.id == activeHttpRequest?.id ? ' (current)' : ''),
(r.id === activeHttpRequest?.id ? ' (current)' : ''),
value: r.id,
};
}),

View File

@@ -1,6 +1,5 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
import React from 'react';
interface Props {
children: ReactNode;

View File

@@ -1,7 +1,7 @@
import type { Environment, Workspace } from '@yaakapp-internal/models';
import { duplicateModel, patchModel } from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai';
import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { createSubEnvironmentAndActivate } from '../commands/createEnvironment';
import { activeWorkspaceAtom, activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
import {
@@ -112,11 +112,11 @@ function EnvironmentEditDialogSidebar({
const treeRef = useRef<TreeHandle>(null);
const { baseEnvironment, baseEnvironments } = useEnvironmentsBreakdown();
// biome-ignore lint/correctness/useExhaustiveDependencies: none
useLayoutEffect(() => {
if (selectedEnvironmentId == null) return;
treeRef.current?.selectItem(selectedEnvironmentId);
treeRef.current?.focus();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleDeleteEnvironment = useCallback(
@@ -137,7 +137,7 @@ function EnvironmentEditDialogSidebar({
enable,
allowDefault: true,
priority: 100,
cb: async function (items: TreeModel[]) {
cb: async (items: TreeModel[]) => {
const item = items[0];
if (items.length === 1 && item != null) {
treeRef.current?.renameItem(item.id);
@@ -152,9 +152,9 @@ function EnvironmentEditDialogSidebar({
'sidebar.selected.duplicate': {
priority: 100,
enable,
cb: async function (items: TreeModel[]) {
if (items.length === 1) {
const item = items[0]!;
cb: async (items: TreeModel[]) => {
if (items.length === 1 && items[0]) {
const item = items[0];
const newId = await duplicateModel(item);
setSelectedEnvironmentId(newId);
} else {

View File

@@ -13,13 +13,13 @@ import {
setupOrConfigureEncryption,
withEncryptionEnabled,
} from '../lib/setupOrConfigureEncryption';
import { PillButton } from './core/PillButton';
import { DismissibleBanner } from './core/DismissibleBanner';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import { Heading } from './core/Heading';
import type { PairEditorHandle, PairWithId } from './core/PairEditor';
import { ensurePairId } from './core/PairEditor.util';
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
import { PillButton } from './core/PillButton';
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip';

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { IconTooltip } from './core/IconTooltip';
export function EnvironmentSharableTooltip() {

View File

@@ -55,6 +55,7 @@ function ExportDataDialogContent({
const handleToggleAll = () => {
setSelectedWorkspaces(
// biome-ignore lint/performance/noAccumulatingSpread: none
allSelected ? {} : workspaces.reduce((acc, w) => ({ ...acc, [w.id]: true }), {}),
);
};

View File

@@ -2,7 +2,7 @@ import type { Folder, GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakap
import { foldersAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import type { CSSProperties } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import { useCallback, useMemo } from 'react';
import { allRequestsAtom } from '../hooks/useAllRequests';
import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse';
@@ -43,7 +43,14 @@ export function FolderLayout({ folder, style }: Props) {
<Icon icon="folder" size="xl" color="secondary" />
<Heading level={1}>{resolvedModelName(folder)}</Heading>
<HStack className="ml-auto" alignItems="center">
<Button rightSlot={<Icon icon="send_horizontal" />} color="secondary" size="sm" variant="border">Send All</Button>
<Button
rightSlot={<Icon icon="send_horizontal" />}
color="secondary"
size="sm"
variant="border"
>
Send All
</Button>
</HStack>
</HStack>
<Separator className="mt-3 mb-8" />
@@ -57,7 +64,7 @@ export function FolderLayout({ folder, style }: Props) {
}
function ChildCard({ child }: { child: Folder | HttpRequest | GrpcRequest | WebsocketRequest }) {
let card;
let card: ReactNode;
if (child.model === 'folder') {
card = <FolderCard folder={child} />;
} else if (child.model === 'http_request') {
@@ -67,7 +74,7 @@ function ChildCard({ child }: { child: Folder | HttpRequest | GrpcRequest | Webs
} else if (child.model === 'websocket_request') {
card = <RequestCard request={child} />;
} else {
card = <div>Unknown model {child['model']}</div>;
card = <div>Unknown model</div>;
}
const navigate = useCallback(async () => {

View File

@@ -1,6 +1,6 @@
import { createWorkspaceModel, foldersAtom, patchModel } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import React, { useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { useAuthTab } from '../hooks/useAuthTab';
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
import { useHeadersTab } from '../hooks/useHeadersTab';
@@ -28,7 +28,11 @@ const TAB_HEADERS = 'headers';
const TAB_VARIABLES = 'variables';
const TAB_GENERAL = 'general';
export type FolderSettingsTab = typeof TAB_AUTH | typeof TAB_HEADERS | typeof TAB_GENERAL | typeof TAB_VARIABLES;
export type FolderSettingsTab =
| typeof TAB_AUTH
| typeof TAB_HEADERS
| typeof TAB_GENERAL
| typeof TAB_VARIABLES;
export function FolderSettingsDialog({ folderId, tab }: Props) {
const folders = useAtomValue(foldersAtom);

View File

@@ -49,7 +49,7 @@ export function FormMultipartEditor({ request, forceUpdateKey, onChange }: Props
pairs={pairs}
onChange={handleChange}
forceUpdateKey={forceUpdateKey}
stateKey={'multipart.' + request.id}
stateKey={`multipart.${request.id}`}
/>
);
}

View File

@@ -2,7 +2,7 @@ import { patchModel } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import type { CSSProperties } from 'react';
import React, { useEffect, useMemo } from 'react';
import { useEffect, useMemo } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useGrpc } from '../hooks/useGrpc';
import { useGrpcProtoFiles } from '../hooks/useGrpcProtoFiles';

View File

@@ -6,11 +6,11 @@ import { useGrpcProtoFiles } from '../hooks/useGrpcProtoFiles';
import { pluralizeCount } from '../lib/pluralize';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { InlineCode } from './core/InlineCode';
import { Link } from './core/Link';
import { HStack, VStack } from './core/Stacks';
import { Icon } from './core/Icon';
interface Props {
onDone: () => void;
@@ -20,7 +20,7 @@ export function GrpcProtoSelectionDialog(props: Props) {
const request = useActiveRequest();
if (request?.model !== 'grpc_request') return null;
return GrpcProtoSelectionDialogWithRequest({ ...props, request });
return <GrpcProtoSelectionDialogWithRequest request={request} {...props} />;
}
function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: GrpcRequest }) {
@@ -103,7 +103,7 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
Found services{' '}
{services?.slice(0, 5).map((s, i) => {
return (
<span key={i}>
<span key={s.name + s.methods.join(',')}>
<InlineCode>{s.name}</InlineCode>
{i === services.length - 1 ? '' : i === services.length - 2 ? ' and ' : ', '}
</span>
@@ -119,7 +119,7 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
Server reflection found services
{services?.map((s, i) => {
return (
<span key={i}>
<span key={s.name + s.methods.join(',')}>
<InlineCode>{s.name}</InlineCode>
{i === services.length - 1 ? '' : i === services.length - 2 ? ' and ' : ', '}
</span>
@@ -135,13 +135,16 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
<table className="w-full divide-y divide-surface-highlight">
<thead>
<tr>
<th className="text-text-subtlest" colSpan={3}>Added File Paths</th>
<th className="text-text-subtlest" colSpan={3}>
Added File Paths
</th>
</tr>
</thead>
<tbody className="divide-y divide-surface-highlight">
{protoFiles.map((f, i) => {
const parts = f.split('/');
return (
// biome-ignore lint/suspicious/noArrayIndexKey: none
<tr key={f + i} className="group">
<td>
<Icon icon={f.endsWith('.proto') ? 'file_code' : 'folder_code'} />

View File

@@ -1,7 +1,7 @@
import { type GrpcRequest, type HttpRequestHeader, patchModel } from '@yaakapp-internal/models';
import classNames from 'classnames';
import type { CSSProperties } from 'react';
import React, { useCallback, useMemo, useRef } from 'react';
import { useCallback, useMemo, useRef } from 'react';
import { useAuthTab } from '../hooks/useAuthTab';
import { useContainerSize } from '../hooks/useContainerQuery';
import type { ReflectResponseService } from '../hooks/useGrpc';
@@ -183,7 +183,7 @@ export function GrpcRequestPane({
onUrlChange={handleChangeUrl}
onCancel={onCancel}
isLoading={isStreaming}
stateKey={'grpc_url.' + activeRequest.id}
stateKey={`grpc_url.${activeRequest.id}`}
/>
<HStack space={1.5}>
<RadioDropdown

View File

@@ -55,13 +55,13 @@ export function GrpcResponsePane({ style, methodType, activeRequest }: Props) {
[activeEventId, events],
);
// Set active message to the first message received if unary
// Set the active message to the first message received if unary
// biome-ignore lint/correctness/useExhaustiveDependencies: none
useEffect(() => {
if (events.length === 0 || activeEvent != null || methodType !== 'unary') {
return;
}
setActiveEventId(events.find((m) => m.eventType === 'server_message')?.id ?? null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [events.length]);
return (
@@ -226,6 +226,7 @@ function EventRow({
return (
<div className="px-1" ref={ref}>
<button
type="button"
onClick={onClick}
className={classNames(
'w-full grid grid-cols-[auto_minmax(0,3fr)_auto] gap-2 items-center text-left',
@@ -274,7 +275,7 @@ function EventRow({
{error && <span className="text-warning"> ({error})</span>}
</div>
<div className={classNames('opacity-50 text-xs')}>
{format(createdAt + 'Z', 'HH:mm:ss.SSS')}
{format(`${createdAt}Z`, 'HH:mm:ss.SSS')}
</div>
</button>
</div>

View File

@@ -1,20 +1,16 @@
import { type } from "@tauri-apps/plugin-os";
import { settingsAtom } from "@yaakapp-internal/models";
import classNames from "classnames";
import { useAtomValue } from "jotai";
import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
import React, { useMemo } from "react";
import { useIsFullscreen } from "../hooks/useIsFullscreen";
import {
HEADER_SIZE_LG,
HEADER_SIZE_MD,
WINDOW_CONTROLS_WIDTH,
} from "../lib/constants";
import { WindowControls } from "./WindowControls";
import { type } from '@tauri-apps/plugin-os';
import { settingsAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
import { useMemo } from 'react';
import { useIsFullscreen } from '../hooks/useIsFullscreen';
import { HEADER_SIZE_LG, HEADER_SIZE_MD, WINDOW_CONTROLS_WIDTH } from '../lib/constants';
import { WindowControls } from './WindowControls';
interface HeaderSizeProps extends HTMLAttributes<HTMLDivElement> {
children?: ReactNode;
size: "md" | "lg";
size: 'md' | 'lg';
ignoreControlsSpacing?: boolean;
onlyXWindowControl?: boolean;
hideControls?: boolean;
@@ -35,10 +31,10 @@ export function HeaderSize({
const s = { ...style };
// Set the height (use min-height because scaling font size may make it larger
if (size === "md") s.minHeight = HEADER_SIZE_MD;
if (size === "lg") s.minHeight = HEADER_SIZE_LG;
if (size === 'md') s.minHeight = HEADER_SIZE_MD;
if (size === 'lg') s.minHeight = HEADER_SIZE_LG;
if (type() === "macos") {
if (type() === 'macos') {
if (!isFullscreen) {
// Add large padding for window controls
s.paddingLeft = 72 / settings.interfaceScale;
@@ -63,16 +59,16 @@ export function HeaderSize({
style={finalStyle}
className={classNames(
className,
"pt-[1px]", // Make up for bottom border
"select-none relative",
"w-full border-b border-border-subtle min-w-0",
'pt-[1px]', // Make up for bottom border
'select-none relative',
'w-full border-b border-border-subtle min-w-0',
)}
>
{/* NOTE: This needs display:grid or else the element shrinks (even though scrollable) */}
<div
className={classNames(
"pointer-events-none h-full w-full overflow-x-auto hide-scrollbars grid",
"px-1", // Give it some space on either end for focus outlines
'pointer-events-none h-full w-full overflow-x-auto hide-scrollbars grid',
'px-1', // Give it some space on either end for focus outlines
)}
>
{children}

View File

@@ -48,7 +48,7 @@ export function HeadersEditor({
<div className="pb-2">
{validInheritedHeaders?.map((pair, i) => (
<PairEditorRow
key={pair.id + '.' + i}
key={`${pair.id}.${i}`}
index={i}
disabled
disableDrag
@@ -109,9 +109,8 @@ const valueType = (pair: Pair): InputProps['type'] => {
name === 'set-cookie'
) {
return 'password';
} else {
return 'text';
}
return 'text';
};
const valueAutocomplete = (headerName: string): GenericCompletionConfig | undefined => {

View File

@@ -6,7 +6,7 @@ import type {
Workspace,
} from '@yaakapp-internal/models';
import { patchModel } from '@yaakapp-internal/models';
import React, { useCallback } from 'react';
import { useCallback } from 'react';
import { openFolderSettings } from '../commands/openFolderSettings';
import { openWorkspaceSettings } from '../commands/openWorkspaceSettings';
import { useHttpAuthenticationConfig } from '../hooks/useHttpAuthenticationConfig';
@@ -66,9 +66,8 @@ export function HttpAuthenticationEditor({ model }: Props) {
</Link>
</EmptyStateText>
);
} else {
return <EmptyStateText>No authentication</EmptyStateText>;
}
return <EmptyStateText>No authentication</EmptyStateText>;
}
if (inheritedAuth.authenticationType === 'none') {
@@ -84,6 +83,7 @@ export function HttpAuthenticationEditor({ model }: Props) {
<p>
Inherited from{' '}
<button
type="submit"
className="underline hover:text-text"
onClick={() => {
if (inheritedAuth.model === 'folder') openFolderSettings(inheritedAuth.id, 'auth');

View File

@@ -2,7 +2,6 @@ import type { HttpRequest } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import type { CSSProperties } from 'react';
import React from 'react';
import { useCurrentGraphQLSchema } from '../hooks/useIntrospectGraphQL';
import { workspaceLayoutAtom } from '../lib/atoms';
import type { SlotProps } from './core/SplitLayout';
@@ -17,7 +16,6 @@ interface Props {
style: CSSProperties;
}
export function HttpRequestLayout({ activeRequest, style }: Props) {
const showGraphQLDocExplorer = useAtomValue(showGraphQLDocExplorerAtom);
const graphQLSchema = useCurrentGraphQLSchema(activeRequest);
@@ -56,7 +54,7 @@ export function HttpRequestLayout({ activeRequest, style }: Props) {
<GraphQLDocsExplorer
requestId={activeRequest.id}
schema={graphQLSchema}
className={classNames(orientation == 'horizontal' && '!ml-0')}
className={classNames(orientation === 'horizontal' && '!ml-0')}
style={style}
/>
)}

View File

@@ -4,7 +4,7 @@ import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import { atom, useAtomValue } from 'jotai';
import type { CSSProperties } from 'react';
import React, { lazy, Suspense, useCallback, useMemo, useState } from 'react';
import { lazy, Suspense, useCallback, useMemo, useState } from 'react';
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
import { allRequestsAtom } from '../hooks/useAllRequests';
import { useAuthTab } from '../hooks/useAuthTab';
@@ -37,13 +37,13 @@ import { showToast } from '../lib/toast';
import { BinaryFileEditor } from './BinaryFileEditor';
import { ConfirmLargeRequestBody } from './ConfirmLargeRequestBody';
import { CountBadge } from './core/CountBadge';
import { Editor } from './core/Editor/LazyEditor';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import { Editor } from './core/Editor/LazyEditor';
import { InlineCode } from './core/InlineCode';
import type { Pair } from './core/PairEditor';
import { PlainInput } from './core/PlainInput';
import { TabContent, Tabs } from './core/Tabs/Tabs';
import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText';
import { FormMultipartEditor } from './FormMultipartEditor';
import { FormUrlencodedEditor } from './FormUrlencodedEditor';
@@ -128,9 +128,9 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
const nonEmptyParameters = activeRequest.urlParameters.filter((p) => p.name || p.value);
const items: Pair[] = [...nonEmptyParameters];
for (const name of placeholderNames) {
const index = items.findIndex((p) => p.name === name);
if (index >= 0) {
items[index]!.readOnlyName = true;
const item = items.find((p) => p.name === name);
if (item) {
item.readOnlyName = true;
} else {
items.push({ name, value: '', enabled: true, readOnlyName: true, id: generateId() });
}
@@ -207,7 +207,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
showMethodToast(patch.method);
}
newContentType = bodyType === BODY_TYPE_OTHER ? 'text/plain' : bodyType;
} else if (bodyType == BODY_TYPE_GRAPHQL) {
} else if (bodyType === BODY_TYPE_GRAPHQL) {
patch.method = 'POST';
newContentType = 'application/json';
showMethodToast(patch.method);

View File

@@ -1,7 +1,7 @@
import type { HttpResponse } from '@yaakapp-internal/models';
import classNames from 'classnames';
import type { ComponentType, CSSProperties } from 'react';
import React, { lazy, Suspense, useCallback, useMemo } from 'react';
import { lazy, Suspense, useCallback, useMemo } from 'react';
import { useLocalStorage } from 'react-use';
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
@@ -18,8 +18,8 @@ import { HttpStatusTag } from './core/HttpStatusTag';
import { LoadingIcon } from './core/LoadingIcon';
import { SizeTag } from './core/SizeTag';
import { HStack, VStack } from './core/Stacks';
import type { TabItem} from './core/Tabs/Tabs';
import { Tabs , TabContent} from './core/Tabs/Tabs';
import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText';
import { ErrorBoundary } from './ErrorBoundary';
import { RecentHttpResponsesDropdown } from './RecentHttpResponsesDropdown';

View File

@@ -1,6 +1,6 @@
import { clear, readText } from '@tauri-apps/plugin-clipboard-manager';
import * as m from 'motion/react-m';
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { useImportCurl } from '../hooks/useImportCurl';
import { useWindowFocus } from '../hooks/useWindowFocus';
import { Button } from './core/Button';
@@ -13,6 +13,7 @@ export function ImportCurlButton() {
const importCurl = useImportCurl();
const [isLoading, setIsLoading] = useState(false);
// biome-ignore lint/correctness/useExhaustiveDependencies: none
useEffect(() => {
readText().then(setClipboardText);
}, [focused]);

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import { useState } from 'react';
import { useLocalStorage } from 'react-use';
import { Button } from './core/Button';
import { VStack } from './core/Stacks';

View File

@@ -5,8 +5,8 @@ import { useAtomValue } from 'jotai';
import type { ReactNode } from 'react';
import { openSettings } from '../commands/openSettings';
import { CargoFeature } from './CargoFeature';
import { PillButton } from './core/PillButton';
import type { ButtonProps } from './core/Button';
import { PillButton } from './core/PillButton';
const details: Record<
LicenseCheckStatus['type'],
@@ -52,10 +52,7 @@ function LicenseBadgeCmp() {
}
return (
<PillButton
color={detail.color}
onClick={() => openSettings.mutate('license')}
>
<PillButton color={detail.color} onClick={() => openSettings.mutate('license')}>
{detail.label}
</PillButton>
);

View File

@@ -2,7 +2,6 @@ import { useQuery } from '@tanstack/react-query';
import { convertFileSrc } from '@tauri-apps/api/core';
import { resolveResource } from '@tauri-apps/api/path';
import classNames from 'classnames';
import React from 'react';
interface Props {
src: string;

View File

@@ -89,7 +89,7 @@ const markdownComponents: Partial<Components> = {
},
code(props) {
const { children, className, ref, ...extraProps } = props;
delete extraProps.node;
extraProps.node = undefined;
const match = /language-(\w+)/.exec(className || '');
return match ? (

View File

@@ -1,13 +1,7 @@
import type {
GrpcRequest,
HttpRequest,
WebsocketRequest} from '@yaakapp-internal/models';
import {
patchModel,
workspacesAtom,
} from '@yaakapp-internal/models';
import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
import { patchModel, workspacesAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import React, { useState } from 'react';
import { useState } from 'react';
import { resolvedModelName } from '../lib/resolvedModelName';
import { router } from '../lib/router';
import { showToast } from '../lib/toast';

View File

@@ -2,7 +2,7 @@ import classNames from 'classnames';
import { FocusTrap } from 'focus-trap-react';
import * as m from 'motion/react-m';
import type { ReactNode } from 'react';
import React, { useRef } from 'react';
import { useRef } from 'react';
import { Portal } from './Portal';
interface Props {

View File

@@ -62,7 +62,8 @@
ul {
@apply list-disc;
ul, ol {
ul,
ol {
@apply my-0;
}
}
@@ -70,12 +71,14 @@
ol {
@apply list-decimal;
ol, ul {
ol,
ul {
@apply my-0;
}
}
ol, ul {
ol,
ul {
@apply pl-6;
li p {
@@ -133,27 +136,27 @@
@apply block font-bold mb-1;
@apply text-text-subtlest;
content: 'Note';
content: "Note";
}
&.x-theme-banner--secondary::before {
content: 'Info';
content: "Info";
}
&.x-theme-banner--success::before {
content: 'Tip';
content: "Tip";
}
&.x-theme-banner--notice::before {
content: 'Important';
content: "Important";
}
&.x-theme-banner--warning::before {
content: 'Warning';
content: "Warning";
}
&.x-theme-banner--danger::before {
content: 'Caution';
content: "Caution";
}
}
@@ -201,7 +204,7 @@
figcaption::before {
@apply border-info absolute left-2 top-0 h-3.5 w-6 rounded-bl border-l border-b border-dotted;
content: '';
content: "";
}
}
}

View File

@@ -40,7 +40,7 @@ export function RecentGrpcConnectionsDropdown({
...connections.map((c) => ({
label: (
<HStack space={2}>
{formatDistanceToNowStrict(c.createdAt + 'Z')} ago &bull;{' '}
{formatDistanceToNowStrict(`${c.createdAt}Z`)} ago &bull;{' '}
<span className="font-mono text-sm">{c.elapsed}ms</span>
</HStack>
),

View File

@@ -2,10 +2,10 @@ import classNames from 'classnames';
import { useMemo, useRef } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
import { allRequestsAtom } from '../hooks/useAllRequests';
import { useHotKey } from '../hooks/useHotKey';
import { useKeyboardEvent } from '../hooks/useKeyboardEvent';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { allRequestsAtom } from '../hooks/useAllRequests';
import { jotaiStore } from '../lib/jotai';
import { resolvedModelName } from '../lib/resolvedModelName';
import { router } from '../lib/router';

View File

@@ -44,7 +44,7 @@ export function RecentWebsocketConnectionsDropdown({
...connections.map((c) => ({
label: (
<HStack space={2}>
{formatDistanceToNowStrict(c.createdAt + 'Z')} ago &bull;{' '}
{formatDistanceToNowStrict(`${c.createdAt}Z`)} ago &bull;{' '}
<span className="font-mono text-sm">{c.elapsed}ms</span>
</HStack>
),

View File

@@ -20,7 +20,7 @@ export function RedirectToLatestWorkspace() {
return;
}
(async function () {
(async () => {
const workspaceId = recentWorkspaces[0] ?? workspaces[0]?.id ?? 'n/a';
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? null;
const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? null;
@@ -37,5 +37,5 @@ export function RedirectToLatestWorkspace() {
})();
}, [recentWorkspaces, workspaces, workspaces.length]);
return <></>;
return null;
}

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames';
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import React, { useCallback, useRef, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
const START_DISTANCE = 7;
@@ -72,12 +72,12 @@ export function ResizeHandle({
onResizeEnd?.();
}
moveState.current = { calledStart: false, xStart: e.clientX, yStart: e.clientY, move, up };
moveState.current = { calledStart: false, xStart: e.clientX, yStart: e.clientY, move, up };
document.documentElement.addEventListener('mousemove', move);
document.documentElement.addEventListener('mouseup', up);
},
[moveState, onResizeEnd, onResizeMove, onResizeStart, vertical],
[onResizeEnd, onResizeMove, onResizeStart, vertical],
);
return (

View File

@@ -15,6 +15,7 @@ export function ResponseHeaders({ response }: Props) {
<div className="overflow-auto h-full pb-4">
<KeyValueRows>
{sortedHeaders.map((h, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: none
<KeyValueRow labelColor="primary" key={i} label={h.name}>
{h.value}
</KeyValueRow>

View File

@@ -7,7 +7,7 @@ import { VStack } from './core/Stacks';
export default function RouteError({ error }: { error: unknown }) {
console.log('Error', error);
const stringified = JSON.stringify(error);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: none
const message = (error as any).message ?? stringified;
const stack =
typeof error === 'object' && error != null && 'stack' in error ? String(error.stack) : null;
@@ -18,7 +18,11 @@ export default function RouteError({ error }: { error: unknown }) {
<FormattedError>
{message}
{stack && (
<DetailsBanner color="secondary" className="mt-3 select-auto text-xs" summary="Stack Trace">
<DetailsBanner
color="secondary"
className="mt-3 select-auto text-xs"
summary="Stack Trace"
>
<div className="mt-2 text-xs">{stack}</div>
</DetailsBanner>
)}

View File

@@ -120,7 +120,7 @@ export function SelectFile({
size={size === 'auto' ? 'md' : size}
variant="border"
icon="x"
title={'Unset ' + itemLabel}
title={`Unset ${itemLabel}`}
onClick={handleClear}
/>
)}

View File

@@ -2,7 +2,7 @@ import { useSearch } from '@tanstack/react-router';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { type } from '@tauri-apps/plugin-os';
import classNames from 'classnames';
import React, { useState } from 'react';
import { useState } from 'react';
import { useKeyPressEvent } from 'react-use';
import { appInfo } from '../../lib/appInfo';
import { capitalize } from '../../lib/capitalize';
@@ -10,8 +10,8 @@ import { HStack } from '../core/Stacks';
import type { TabItem } from '../core/Tabs/Tabs';
import { TabContent, Tabs } from '../core/Tabs/Tabs';
import { HeaderSize } from '../HeaderSize';
import { SettingsInterface } from './SettingsInterface';
import { SettingsGeneral } from './SettingsGeneral';
import { SettingsInterface } from './SettingsInterface';
import { SettingsLicense } from './SettingsLicense';
import { SettingsPlugins } from './SettingsPlugins';
import { SettingsProxy } from './SettingsProxy';

View File

@@ -2,8 +2,8 @@ import { revealItemInDir } from '@tauri-apps/plugin-opener';
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { appInfo } from '../../lib/appInfo';
import { useCheckForUpdates } from '../../hooks/useCheckForUpdates';
import { appInfo } from '../../lib/appInfo';
import { revealInFinderText } from '../../lib/reveal';
import { CargoFeature } from '../CargoFeature';
import { Checkbox } from '../core/Checkbox';
@@ -108,8 +108,10 @@ export function SettingsGeneral() {
placeholder="0"
labelPosition="left"
defaultValue={`${workspace.settingRequestTimeout}`}
validate={(value) => parseInt(value) >= 0}
onChange={(v) => patchModel(workspace, { settingRequestTimeout: parseInt(v) || 0 })}
validate={(value) => Number.parseInt(value, 10) >= 0}
onChange={(v) =>
patchModel(workspace, { settingRequestTimeout: Number.parseInt(v, 10) || 0 })
}
type="number"
/>

View File

@@ -4,7 +4,7 @@ import { useLicense } from '@yaakapp-internal/license';
import type { EditorKeymap, Settings } from '@yaakapp-internal/models';
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import React from 'react';
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { clamp } from '../../lib/clamp';
import { showConfirm } from '../../lib/confirm';
@@ -95,7 +95,7 @@ export function SettingsInterface() {
defaultValue="14"
value={`${settings.interfaceFontSize}`}
options={fontSizeOptions}
onChange={(v) => patchModel(settings, { interfaceFontSize: parseInt(v) })}
onChange={(v) => patchModel(settings, { interfaceFontSize: Number.parseInt(v, 10) })}
/>
</HStack>
<HStack space={2} alignItems="end">
@@ -127,7 +127,7 @@ export function SettingsInterface() {
value={`${settings.editorFontSize}`}
options={fontSizeOptions}
onChange={(v) =>
patchModel(settings, { editorFontSize: clamp(parseInt(v) || 14, 8, 30) })
patchModel(settings, { editorFontSize: clamp(Number.parseInt(v, 10) || 14, 8, 30) })
}
/>
</HStack>

View File

@@ -1,7 +1,7 @@
import { openUrl } from '@tauri-apps/plugin-opener';
import { useLicense } from '@yaakapp-internal/license';
import { differenceInDays } from 'date-fns';
import React, { useState } from 'react';
import { useState } from 'react';
import { useToggle } from '../../hooks/useToggle';
import { pluralizeCount } from '../../lib/pluralize';
import { CargoFeature } from '../CargoFeature';
@@ -66,8 +66,7 @@ function SettingsLicenseCmp() {
<br />
<span className="opacity-50">
You may continue using Yaak for personal use free, forever.
<br />
A license is required for commercial use.
<br />A license is required for commercial use.
</span>
<Separator className="my-2" />
<div className="flex flex-wrap items-center gap-x-2 text-sm text-notice">

View File

@@ -10,7 +10,7 @@ import {
uninstallPlugin,
} from '@yaakapp-internal/plugins';
import { useAtomValue } from 'jotai';
import React, { useState } from 'react';
import { useState } from 'react';
import { useDebouncedValue } from '../../hooks/useDebouncedValue';
import { useInstallPlugin } from '../../hooks/useInstallPlugin';
import { usePluginInfo } from '../../hooks/usePluginInfo';
@@ -306,7 +306,7 @@ function usePromptUninstall(pluginId: string | null, name: string) {
if (pluginId == null) return;
const confirmed = await showConfirmDelete({
id: 'uninstall-plugin-' + pluginId,
id: `uninstall-plugin-${pluginId}`,
title: 'Uninstall Plugin',
confirmText: 'Uninstall',
description: (

View File

@@ -1,6 +1,6 @@
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import React from 'react';
import { Checkbox } from '../core/Checkbox';
import { InlineCode } from '../core/InlineCode';
import { PlainInput } from '../core/PlainInput';

View File

@@ -1,6 +1,6 @@
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import React, { lazy, Suspense } from 'react';
import { lazy, Suspense } from 'react';
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { useResolvedAppearance } from '../../hooks/useResolvedAppearance';
import { useResolvedTheme } from '../../hooks/useResolvedTheme';
@@ -127,7 +127,7 @@ export function SettingsTheme() {
color={c}
size="2xs"
iconSize="xs"
icon={icons[i % icons.length]!}
icon={icons[i % icons.length] ?? 'info'}
iconClassName="text"
title={`${c}`}
/>
@@ -139,7 +139,7 @@ export function SettingsTheme() {
variant="border"
size="2xs"
iconSize="xs"
icon={icons[i % icons.length]!}
icon={icons[i % icons.length] ?? 'info'}
iconClassName="text"
title={`${c}`}
/>

View File

@@ -84,7 +84,7 @@ const OPACITY_SUBTLE = 'opacity-80';
function Sidebar({ className }: { className?: string }) {
const [hidden, setHidden] = useSidebarHidden();
const activeWorkspaceId = useAtomValue(activeWorkspaceAtom)?.id;
const treeId = 'tree.' + (activeWorkspaceId ?? 'unknown');
const treeId = `tree.${activeWorkspaceId ?? 'unknown'}`;
const filterText = useAtomValue(sidebarFilterAtom);
const [tree, allFields] = useAtomValue(sidebarTreeAtom) ?? [];
const wrapperRef = useRef<HTMLElement>(null);
@@ -95,8 +95,8 @@ function Sidebar({ className }: { className?: string }) {
}, []);
const allHidden = useMemo(() => {
if (tree?.children?.length === 0) return false;
else if (filterText) return tree?.children?.every((c) => c.hidden);
else return true;
if (filterText) return tree?.children?.every((c) => c.hidden);
return true;
}, [filterText, tree?.children]);
const focusActiveItem = useCallback(() => {
@@ -119,7 +119,7 @@ function Sidebar({ className }: { className?: string }) {
treeRef.current?.selectItem(activeId, true);
}
});
}, [focusActiveItem]);
}, []);
useHotKey(
'sidebar.filter',
@@ -250,12 +250,13 @@ function Sidebar({ className }: { className?: string }) {
if (tree == null) return;
const next = (node: TreeNode<SidebarModel>, collapsed: Record<string, boolean>) => {
let newCollapsed = { ...collapsed };
for (const n of node.children ?? []) {
if (n.item.model !== 'folder') continue;
collapsed[n.item.id] = true;
collapsed = next(n, collapsed);
newCollapsed[n.item.id] = true;
newCollapsed = next(n, collapsed);
}
return collapsed;
return newCollapsed;
};
const collapsed = next(tree, {});
jotaiStore.set(collapsedFamily(treeId), collapsed);
@@ -263,14 +264,14 @@ function Sidebar({ className }: { className?: string }) {
},
'sidebar.selected.delete': {
enable,
cb: async function (items: SidebarModel[]) {
cb: async (items: SidebarModel[]) => {
await deleteModelWithConfirm(items);
},
},
'sidebar.selected.rename': {
enable,
allowDefault: true,
cb: async function (items: SidebarModel[]) {
cb: async (items: SidebarModel[]) => {
const item = items[0];
if (items.length === 1 && item != null) {
treeRef.current?.renameItem(item.id);
@@ -280,9 +281,9 @@ function Sidebar({ className }: { className?: string }) {
'sidebar.selected.duplicate': {
priority: 10,
enable,
cb: async function (items: SidebarModel[]) {
if (items.length === 1) {
const item = items[0]!;
cb: async (items: SidebarModel[]) => {
if (items.length === 1 && items[0]) {
const item = items[0];
const newId = await duplicateModel(item);
navigateToRequestOrFolderOrWorkspace(newId, item.model);
} else {
@@ -292,7 +293,7 @@ function Sidebar({ className }: { className?: string }) {
},
'request.send': {
enable,
cb: async function (items: SidebarModel[]) {
cb: async (items: SidebarModel[]) => {
await Promise.all(
items
.filter((i) => i.model === 'http_request')
@@ -355,8 +356,7 @@ function Sidebar({ className }: { className?: string }) {
: []
).map((a) => ({
label: a.label,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
leftSlot: <Icon icon={(a.icon as any) ?? 'empty'} />,
leftSlot: <Icon icon={a.icon ?? 'empty'} />,
onSelect: async () => {
const request = getModel('http_request', child.id);
if (request != null) await a.call(request);
@@ -368,7 +368,7 @@ function Sidebar({ className }: { className?: string }) {
).map((a) => ({
label: a.label,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
leftSlot: <Icon icon={(a.icon as any) ?? 'empty'} />,
leftSlot: <Icon icon={a.icon ?? 'empty'} />,
onSelect: async () => {
const request = getModel('grpc_request', child.id);
if (request != null) await a.call(request);
@@ -621,10 +621,10 @@ const sidebarTreeAtom = atom<[TreeNode<SidebarModel>, FieldDef[]] | null>((get)
for (const item of allModels) {
if ('folderId' in item && item.folderId == null) {
childrenMap[item.workspaceId] = childrenMap[item.workspaceId] ?? [];
childrenMap[item.workspaceId]!.push(item);
childrenMap[item.workspaceId]?.push(item);
} else if ('folderId' in item && item.folderId != null) {
childrenMap[item.folderId] = childrenMap[item.folderId] ?? [];
childrenMap[item.folderId]!.push(item);
childrenMap[item.folderId]?.push(item);
}
}
@@ -729,18 +729,18 @@ const SidebarLeftSlot = memo(function SidebarLeftSlot({
}) {
if (item.model === 'folder') {
return <Icon icon="folder" />;
} else if (item.model === 'workspace') {
return null;
} else {
const isSelected = jotaiStore.get(isSelectedFamily({ treeId, itemId: item.id }));
return (
<HttpMethodTag
short
className={classNames('text-xs pl-1.5', !isSelected && OPACITY_SUBTLE)}
request={item}
/>
);
}
if (item.model === 'workspace') {
return null;
}
const isSelected = jotaiStore.get(isSelectedFamily({ treeId, itemId: item.id }));
return (
<HttpMethodTag
short
className={classNames('text-xs pl-1.5', !isSelected && OPACITY_SUBTLE)}
request={item}
/>
);
});
const SidebarInnerItem = memo(function SidebarInnerItem({

View File

@@ -2,9 +2,9 @@ import { useMemo } from 'react';
import { useFloatingSidebarHidden } from '../hooks/useFloatingSidebarHidden';
import { useShouldFloatSidebar } from '../hooks/useShouldFloatSidebar';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { CreateDropdown } from './CreateDropdown';
import { IconButton } from './core/IconButton';
import { HStack } from './core/Stacks';
import { CreateDropdown } from './CreateDropdown';
export function SidebarActions() {
const floating = useShouldFloatSidebar();

View File

@@ -50,7 +50,7 @@ export function TemplateFunctionDialog({ initialTokens, templateFunction, ...pro
return;
}
(async function () {
(async () => {
const initial = collectArgumentValues(initialTokens, templateFunction);
// HACK: Replace the secure() function's encrypted `value` arg with the decrypted version so
@@ -140,6 +140,7 @@ function InitializedTemplateFunctionDialog({
);
const tooLarge = rendered.data ? rendered.data.length > 10000 : false;
// biome-ignore lint/correctness/useExhaustiveDependencies: Only update this on rendered data change to keep secrets hidden on input change
const dataContainsSecrets = useMemo(() => {
for (const [name, value] of Object.entries(argValues)) {
const arg = templateFunction.data?.args.find((a) => 'name' in a && a.name === name);
@@ -149,8 +150,6 @@ function InitializedTemplateFunctionDialog({
}
}
return false;
// Only update this on rendered data change to keep secrets hidden on input change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [rendered.data]);
if (templateFunction.data == null || templateFunction.isPending) {
@@ -177,7 +176,7 @@ function InitializedTemplateFunctionDialog({
name="value"
type="password"
placeholder="••••••••••••"
defaultValue={String(argValues['value'] ?? '')}
defaultValue={String(argValues.value ?? '')}
onChange={(value) => setArgValues({ ...argValues, value })}
/>
) : (
@@ -211,27 +210,28 @@ function InitializedTemplateFunctionDialog({
)}
/>
</HStack>
<InlineCode
className={classNames(
'relative',
'whitespace-pre-wrap !select-text cursor-text max-h-[10rem] overflow-auto hide-scrollbars !border-text-subtlest',
tooLarge && 'italic text-danger',
)}
>
{rendered.error || tagText.error ? (
<em className="text-danger">
{`${rendered.error || tagText.error}`.replace(/^Render Error: /, '')}
</em>
) : dataContainsSecrets && !showSecretsInPreview ? (
<span className="italic text-text-subtle">
------ sensitive values hidden ------
</span>
) : tooLarge ? (
'too large to preview'
) : (
rendered.data || <>&nbsp;</>
)}
<div className="absolute right-0 top-0 flex items-center">
<div className="relative w-full max-h-[10rem]">
<InlineCode
className={classNames(
'block whitespace-pre-wrap !select-text cursor-text max-h-[10rem] overflow-auto hide-scrollbars !border-text-subtlest',
tooLarge && 'italic text-danger',
)}
>
{rendered.error || tagText.error ? (
<em className="text-danger">
{`${rendered.error || tagText.error}`.replace(/^Render Error: /, '')}
</em>
) : dataContainsSecrets && !showSecretsInPreview ? (
<span className="italic text-text-subtle">
------ sensitive values hidden ------
</span>
) : tooLarge ? (
'too large to preview'
) : (
rendered.data || <>&nbsp;</>
)}
</InlineCode>
<div className="absolute right-0.5 top-0 bottom-0 flex items-center">
<IconButton
size="xs"
icon="refresh"
@@ -243,7 +243,7 @@ function InitializedTemplateFunctionDialog({
}}
/>
</div>
</InlineCode>
</div>
</div>
) : (
<span />
@@ -263,22 +263,23 @@ function InitializedTemplateFunctionDialog({
);
}
TemplateFunctionDialog.show = function (
TemplateFunctionDialog.show = (
fn: TemplateFunction,
tagValue: string,
startPos: number,
view: EditorView,
) {
) => {
const initialTokens = parseTemplate(tagValue);
showDialog({
id: 'template-function-' + Math.random(), // Allow multiple at once
id: `template-function-${Math.random()}`, // Allow multiple at once
size: 'md',
className: 'h-[60rem]',
noPadding: true,
title: <InlineCode>{fn.name}()</InlineCode>,
description: fn.description,
render: ({ hide }) => {
const model = jotaiStore.get(activeWorkspaceAtom)!;
const model = jotaiStore.get(activeWorkspaceAtom);
if (model == null) return null;
return (
<TemplateFunctionDialog
templateFunction={fn}

View File

@@ -1,10 +1,10 @@
import { AnimatePresence } from 'motion/react';
import { useAtomValue } from 'jotai';
import React, { type ReactNode } from 'react';
import { AnimatePresence } from 'motion/react';
import type { ReactNode } from 'react';
import { hideToast, toastsAtom } from '../lib/toast';
import { Toast, type ToastProps } from './core/Toast';
import { Portal } from './Portal';
import { ErrorBoundary } from './ErrorBoundary';
import { Portal } from './Portal';
export type ToastInstance = {
id: string;

View File

@@ -1,7 +1,7 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import classNames from 'classnames';
import type { FormEvent, ReactNode } from 'react';
import { useCallback, memo, useRef, useState } from 'react';
import { memo, useCallback, useRef, useState } from 'react';
import { useHotKey } from '../hooks/useHotKey';
import type { IconProps } from './core/Icon';
import { IconButton } from './core/IconButton';

View File

@@ -15,7 +15,7 @@ type Props = {
export function UrlParametersEditor({ pairs, forceUpdateKey, onChange, stateKey }: Props) {
const pairEditorRef = useRef<PairEditorHandle>(null);
const handleInitPairEditorRef = useCallback((ref: PairEditorHandle) => {
return (pairEditorRef.current = ref);
pairEditorRef.current = ref;
}, []);
const [{ urlParametersKey }] = useRequestEditor();

View File

@@ -2,7 +2,7 @@ import type { WebsocketRequest } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import type { CSSProperties } from 'react';
import React from 'react';
import { workspaceLayoutAtom } from '../lib/atoms';
import { SplitLayout } from './core/SplitLayout';
import { WebsocketRequestPane } from './WebsocketRequestPane';

View File

@@ -5,7 +5,7 @@ import { closeWebsocket, connectWebsocket, sendWebsocket } from '@yaakapp-intern
import classNames from 'classnames';
import { atom, useAtomValue } from 'jotai';
import type { CSSProperties } from 'react';
import React, { useCallback, useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { getActiveCookieJar } from '../hooks/useActiveCookieJar';
import { getActiveEnvironment } from '../hooks/useActiveEnvironment';
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
@@ -25,8 +25,8 @@ import { generateId } from '../lib/generateId';
import { prepareImportQuerystring } from '../lib/prepareImportQuerystring';
import { resolvedModelName } from '../lib/resolvedModelName';
import { CountBadge } from './core/CountBadge';
import { Editor } from './core/Editor/LazyEditor';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import { Editor } from './core/Editor/LazyEditor';
import { IconButton } from './core/IconButton';
import type { Pair } from './core/PairEditor';
import { PlainInput } from './core/PlainInput';
@@ -81,9 +81,9 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
const nonEmptyParameters = activeRequest.urlParameters.filter((p) => p.name || p.value);
const items: Pair[] = [...nonEmptyParameters];
for (const name of placeholderNames) {
const index = items.findIndex((p) => p.name === name);
if (index >= 0) {
items[index]!.readOnlyName = true;
const item = items.find((p) => p.name === name);
if (item) {
item.readOnlyName = true;
} else {
items.push({ name, value: '', enabled: true, readOnlyName: true, id: generateId() });
}

View File

@@ -135,7 +135,7 @@ export function WebsocketResponsePane({ activeRequest }: Props) {
? 'Connection open'
: `Message ${activeEvent.isServer ? 'Received' : 'Sent'}`}
</div>
{message != '' && (
{message !== '' && (
<HStack space={1}>
<Button
variant="border"
@@ -215,6 +215,7 @@ function EventRow({
return (
<div className="px-1" ref={ref}>
<button
type="button"
onClick={onClick}
className={classNames(
'w-full grid grid-cols-[auto_minmax(0,3fr)_auto] gap-2 items-center text-left',
@@ -252,7 +253,7 @@ function EventRow({
{/*{error && <span className="text-warning"> ({error})</span>}*/}
</div>
<div className={classNames('opacity-50 text-xs')}>
{format(createdAt + 'Z', 'HH:mm:ss.SSS')}
{format(`${createdAt}Z`, 'HH:mm:ss.SSS')}
</div>
</button>
</div>

View File

@@ -3,7 +3,7 @@ import { type } from '@tauri-apps/plugin-os';
import { settingsAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import React, { useState } from 'react';
import { useState } from 'react';
import { WINDOW_CONTROLS_WIDTH } from '../lib/constants';
import { Button } from './core/Button';
import { HStack } from './core/Stacks';
@@ -37,6 +37,7 @@ export function WindowControls({ className, onlyX }: Props) {
onClick={() => getCurrentWebviewWindow().minimize()}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>Minimize</title>
<path fill="currentColor" d="M14 8v1H3V8z" />
</svg>
</Button>
@@ -57,6 +58,7 @@ export function WindowControls({ className, onlyX }: Props) {
>
{maximized ? (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>Unmaximize</title>
<g fill="currentColor">
<path d="M3 5v9h9V5zm8 8H4V6h7z" />
<path fillRule="evenodd" d="M5 5h1V4h7v7h-1v1h2V3H5z" clipRule="evenodd" />
@@ -64,6 +66,7 @@ export function WindowControls({ className, onlyX }: Props) {
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>Maximize</title>
<path fill="currentColor" d="M3 3v10h10V3zm9 9H4V4h8z" />
</svg>
)}
@@ -76,6 +79,7 @@ export function WindowControls({ className, onlyX }: Props) {
onClick={() => getCurrentWebviewWindow().close()}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>Close</title>
<path
fill="currentColor"
fillRule="evenodd"

View File

@@ -30,12 +30,12 @@ import { useSyncWorkspaceRequestTitle } from '../hooks/useSyncWorkspaceRequestTi
import { duplicateRequestOrFolderAndNavigate } from '../lib/duplicateRequestOrFolderAndNavigate';
import { importData } from '../lib/importData';
import { jotaiStore } from '../lib/jotai';
import { CreateDropdown } from './CreateDropdown';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { HotKeyList } from './core/HotKeyList';
import { FeedbackLink } from './core/Link';
import { HStack } from './core/Stacks';
import { CreateDropdown } from './CreateDropdown';
import { ErrorBoundary } from './ErrorBoundary';
import { FolderLayout } from './FolderLayout';
import { GrpcConnectionLayout } from './GrpcConnectionLayout';
@@ -221,11 +221,14 @@ function WorkspaceBody() {
if (activeRequest?.model === 'grpc_request') {
return <GrpcConnectionLayout style={body} />;
} else if (activeRequest?.model === 'websocket_request') {
}
if (activeRequest?.model === 'websocket_request') {
return <WebsocketRequestLayout style={body} activeRequest={activeRequest} />;
} else if (activeRequest?.model === 'http_request') {
}
if (activeRequest?.model === 'http_request') {
return <HttpRequestLayout activeRequest={activeRequest} style={body} />;
} else if (activeFolder != null) {
}
if (activeFolder != null) {
return <FolderLayout folder={activeFolder} style={body} />;
}

View File

@@ -104,7 +104,8 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
// Always open a new window if the selected one is already active
switchWorkspace.mutate({ workspaceId, inNewWindow: true });
return;
} else if (typeof settings.openWorkspaceNewWindow === 'boolean') {
}
if (typeof settings.openWorkspaceNewWindow === 'boolean') {
switchWorkspace.mutate({ workspaceId, inNewWindow: settings.openWorkspaceNewWindow });
return;
}

View File

@@ -50,7 +50,7 @@ export function WorkspaceEncryptionSetting({ size, expanded, onDone, onEnabledEn
setKey({ key: null, error: `${err}` });
},
);
}, [setKey, workspaceMeta, workspaceMeta?.encryptionKey]);
}, [workspaceMeta, workspaceMeta?.encryptionKey]);
if (key == null || workspace == null || workspaceMeta == null) {
return null;
@@ -117,7 +117,7 @@ export function WorkspaceEncryptionSetting({ size, expanded, onDone, onEnabledEn
await enableEncryption(workspaceMeta.workspaceId);
setJustEnabledEncryption(true);
} catch (err) {
setError('Failed to enable encryption: ' + err);
setError(`Failed to enable encryption: ${err}`);
}
}}
>
@@ -242,10 +242,11 @@ function HighlightedKey({ keyText, show }: { keyText: string; show: boolean }) {
keyText.split('').map((c, i) => {
return (
<span
// biome-ignore lint/suspicious/noArrayIndexKey: it's fine
key={i}
className={classNames(
c.match(/[0-9]/) && 'text-info',
c == '-' && 'text-text-subtle',
c === '-' && 'text-text-subtle',
)}
>
{c}

View File

@@ -1,14 +1,14 @@
import classNames from 'classnames';
import { useAtom, useAtomValue } from 'jotai';
import React, { memo } from 'react';
import { memo } from 'react';
import { activeWorkspaceAtom, activeWorkspaceMetaAtom } from '../hooks/useActiveWorkspace';
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';
import { workspaceLayoutAtom } from '../lib/atoms';
import { setupOrConfigureEncryption } from '../lib/setupOrConfigureEncryption';
import { CookieDropdown } from './CookieDropdown';
import { PillButton } from './core/PillButton';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { PillButton } from './core/PillButton';
import { HStack } from './core/Stacks';
import { EnvironmentActionsDropdown } from './EnvironmentActionsDropdown';
import { ImportCurlButton } from './ImportCurlButton';
@@ -37,7 +37,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
'grid grid-cols-[auto_minmax(0,1fr)_auto] items-center w-full h-full',
)}
>
<HStack space={0.5} className={classNames("flex-1 pointer-events-none")}>
<HStack space={0.5} className={classNames('flex-1 pointer-events-none')}>
<SidebarActions />
<CookieDropdown />
<HStack className="min-w-0">

View File

@@ -39,6 +39,8 @@ export function AutoScroller<T>({ data, render, header }: Props<T>) {
useLayoutEffect(() => {
if (!autoScroll) return;
data.length; // Make linter happy. We want to refresh when length changes
const el = containerRef.current;
if (el == null) return;
@@ -69,21 +71,26 @@ export function AutoScroller<T>({ data, render, header }: Props<T>) {
position: 'relative',
}}
>
{rowVirtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{render(data[virtualItem.index]!, virtualItem.index)}
</div>
))}
{rowVirtualizer.getVirtualItems().map((virtualItem) => {
const item = data[virtualItem.index];
return (
item != null && (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{render(item, virtualItem.index)}
</div>
)
);
})}
</div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import classNames from 'classnames';
import { type ReactNode } from 'react';
import type { ReactNode } from 'react';
import { Icon } from './Icon';
import { IconTooltip } from './IconTooltip';
import { HStack } from './Stacks';
@@ -58,7 +58,9 @@ export function Checkbox({
</div>
</div>
{!hideLabel && (
<div className={classNames('text-sm', fullWidth && 'w-full', disabled && 'opacity-disabled')}>
<div
className={classNames('text-sm', fullWidth && 'w-full', disabled && 'opacity-disabled')}
>
{title}
</div>
)}

View File

@@ -1,10 +1,10 @@
import type { Color } from "@yaakapp-internal/plugins";
import type { FormEvent } from "react";
import { useState } from "react";
import { CopyIconButton } from "../CopyIconButton";
import { Button } from "./Button";
import { PlainInput } from "./PlainInput";
import { HStack } from "./Stacks";
import type { Color } from '@yaakapp-internal/plugins';
import type { FormEvent } from 'react';
import { useState } from 'react';
import { CopyIconButton } from '../CopyIconButton';
import { Button } from './Button';
import { PlainInput } from './PlainInput';
import { HStack } from './Stacks';
export interface ConfirmProps {
onHide: () => void;
@@ -19,9 +19,9 @@ export function Confirm({
onResult,
confirmText,
requireTyping,
color = "primary",
color = 'primary',
}: ConfirmProps) {
const [confirm, setConfirm] = useState<string>("");
const [confirm, setConfirm] = useState<string>('');
const handleHide = () => {
onResult(false);
onHide();
@@ -61,13 +61,9 @@ export function Confirm({
}
/>
)}
<HStack
space={2}
justifyContent="start"
className="mt-2 mb-4 flex-row-reverse"
>
<HStack space={2} justifyContent="start" className="mt-2 mb-4 flex-row-reverse">
<Button type="submit" color={color} disabled={!didConfirm}>
{confirmText ?? "Confirm"}
{confirmText ?? 'Confirm'}
</Button>
<Button onClick={handleHide} variant="border">
Cancel

View File

@@ -24,9 +24,7 @@ export function DetailsBanner({ className, color, summary, children, ...extraPro
/>
{summary}
</summary>
<div className="mt-1.5">
{children}
</div>
<div className="mt-1.5">{children}</div>
</details>
</Banner>
);

View File

@@ -43,7 +43,6 @@ export function Dialog({
return (
<Overlay open={open} onClose={disableBackdropClose ? undefined : onClose} portalName="dialog">
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
<div
role="dialog"
className={classNames(

View File

@@ -12,7 +12,10 @@ export function DismissibleBanner({
id,
actions,
...props
}: BannerProps & { id: string; actions?: { label: string; onClick: () => void; color?: Color }[] }) {
}: BannerProps & {
id: string;
actions?: { label: string; onClick: () => void; color?: Color }[];
}) {
const { set: setDismissed, value: dismissed } = useKeyValue<boolean>({
namespace: 'global',
key: ['dismiss-banner', id],
@@ -28,9 +31,9 @@ export function DismissibleBanner({
>
{children}
<HStack space={1.5}>
{actions?.map((a, i) => (
{actions?.map((a) => (
<Button
key={a.label + i}
key={a.label}
variant="border"
color={a.color ?? props.color}
size="xs"

View File

@@ -3,15 +3,16 @@ import { atom } from 'jotai';
import * as m from 'motion/react-m';
import type {
CSSProperties,
FocusEvent as ReactFocusEvent,
HTMLAttributes,
MouseEvent,
ReactElement,
FocusEvent as ReactFocusEvent,
KeyboardEvent as ReactKeyboardEvent,
ReactNode,
RefObject,
SetStateAction,
} from 'react';
import React, {
import {
Children,
cloneElement,
forwardRef,
@@ -30,6 +31,7 @@ import { useStateWithDeps } from '../../hooks/useStateWithDeps';
import { generateId } from '../../lib/generateId';
import { getNodeText } from '../../lib/getNodeText';
import { jotaiStore } from '../../lib/jotai';
import { ErrorBoundary } from '../ErrorBoundary';
import { Overlay } from '../Overlay';
import { Button } from './Button';
import { HotKey } from './HotKey';
@@ -37,7 +39,6 @@ import { Icon } from './Icon';
import { LoadingIcon } from './LoadingIcon';
import { Separator } from './Separator';
import { HStack, VStack } from './Stacks';
import { ErrorBoundary } from '../ErrorBoundary';
export type DropdownItemSeparator = {
type: 'separator';
@@ -96,24 +97,24 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
ref,
) {
const id = useRef(generateId());
const [isOpen, _setIsOpen] = useState<boolean>(false);
const [isOpen, setIsOpen] = useState<boolean>(false);
useEffect(() => {
return jotaiStore.sub(openAtom, () => {
const globalOpenId = jotaiStore.get(openAtom);
const newIsOpen = globalOpenId === id.current;
if (newIsOpen !== isOpen) {
_setIsOpen(newIsOpen);
setIsOpen(newIsOpen);
}
});
}, [isOpen, _setIsOpen]);
}, [isOpen]);
// const [isOpen, _setIsOpen] = useState<boolean>(false);
const [defaultSelectedIndex, setDefaultSelectedIndex] = useState<number | null>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const menuRef = useRef<Omit<DropdownRef, 'open'>>(null);
const setIsOpen = useCallback(
const handleSetIsOpen = useCallback(
(o: SetStateAction<boolean>) => {
jotaiStore.set(openAtom, (prevId) => {
const prevIsOpen = prevId === id.current;
@@ -121,9 +122,11 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
// Persist background color of button until we close the dropdown
if (newIsOpen) {
onOpen?.();
buttonRef.current!.style.backgroundColor = window
.getComputedStyle(buttonRef.current!)
.getPropertyValue('background-color');
if (buttonRef.current) {
buttonRef.current.style.backgroundColor = window
.getComputedStyle(buttonRef.current)
.getPropertyValue('background-color');
}
}
return newIsOpen ? id.current : null; // Set global atom to current ID to signify open state
});
@@ -136,7 +139,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
useEffect(() => {
if (!isOpen) {
// Clear persisted BG
buttonRef.current!.style.backgroundColor = '';
if (buttonRef.current) buttonRef.current.style.backgroundColor = '';
// Set to different value when opened and closed to force it to update. This is to force
// <Menu/> to reset its selected-index state, which it does when this prop changes
setDefaultSelectedIndex(null);
@@ -157,38 +160,38 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
else this.close();
},
open(index?: number) {
setIsOpen(true);
handleSetIsOpen(true);
setDefaultSelectedIndex(index ?? -1);
},
close() {
setIsOpen(false);
handleSetIsOpen(false);
},
}),
[isOpen, setIsOpen, menuRefCurrent],
[isOpen, handleSetIsOpen, menuRefCurrent],
);
useHotKey(hotKeyAction ?? null, () => {
setDefaultSelectedIndex(0);
setIsOpen(true);
handleSetIsOpen(true);
});
const child = useMemo(() => {
const existingChild = Children.only(children);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const props: any = {
...existingChild.props,
ref: buttonRef,
'aria-haspopup': 'true',
onClick:
existingChild.props?.onClick ??
((e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
setIsOpen((o) => !o); // Toggle dropdown
}),
};
const props: HTMLAttributes<HTMLButtonElement> & { ref: RefObject<HTMLButtonElement | null> } =
{
...existingChild.props,
ref: buttonRef,
'aria-haspopup': 'true',
onClick:
existingChild.props?.onClick ??
((e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
handleSetIsOpen((o) => !o); // Toggle dropdown
}),
};
return cloneElement(existingChild, props);
}, [children, setIsOpen]);
}, [children, handleSetIsOpen]);
useEffect(() => {
buttonRef.current?.setAttribute('aria-expanded', isOpen.toString());
@@ -204,7 +207,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
return (
<>
{child}
<ErrorBoundary name={`Dropdown Menu`}>
<ErrorBoundary name={'Dropdown Menu'}>
<Menu
ref={menuRef}
showTriangle
@@ -213,7 +216,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
defaultSelectedIndex={defaultSelectedIndex}
items={items}
triggerShape={triggerRect ?? null}
onClose={() => setIsOpen(false)}
onClose={() => handleSetIsOpen(false)}
isOpen={isOpen}
/>
</ErrorBoundary>
@@ -303,7 +306,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
}, [onClose]);
// Close menu on space bar
const handleMenuKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
const handleMenuKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>) => {
const isCharacter = e.key.length === 1;
const isSpecial = e.ctrlKey || e.metaKey || e.altKey;
if (isCharacter && !isSpecial) {
@@ -348,7 +351,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
);
const handleNext = useCallback(
(incrBy: number = 1) => {
(incrBy = 1) => {
setSelectedIndex((currIndex) => {
let nextIndex = (currIndex ?? -1) + incrBy;
const maxTries = items.length;
@@ -474,7 +477,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
const handleFocus = useCallback(
(i: DropdownItem) => {
const index = filteredItems.findIndex((item) => item === i) ?? null;
const index = filteredItems.indexOf(i) ?? null;
setSelectedIndex(index);
},
[filteredItems, setSelectedIndex],
@@ -556,6 +559,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
if (item.type === 'separator') {
return (
<Separator
// biome-ignore lint/suspicious/noArrayIndexKey: Nothing else available
key={i}
className={classNames('my-1.5', item.label ? 'ml-2' : null)}
>
@@ -565,12 +569,9 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
}
if (item.type === 'content') {
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions,jsx-a11y/click-events-have-key-events
<div
key={i}
className={classNames('my-1 mx-2 max-w-xs')}
onClick={onClose}
>
// biome-ignore lint/a11y/noStaticElementInteractions: Needs to be clickable but want to support nested buttons
// biome-ignore lint/suspicious/noArrayIndexKey: index is fine
<div key={i} className={classNames('my-1 mx-2 max-w-xs')} onClick={onClose}>
{item.label}
</div>
);
@@ -580,7 +581,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
focused={i === selectedIndex}
onFocus={handleFocus}
onSelect={handleSelect}
key={`item_${i}`}
key={`item_${item.label}`}
item={item}
/>
);

View File

@@ -7,8 +7,7 @@ export class BetterMatchDecorator extends MatchDecorator {
updateDeco(update: ViewUpdate, deco: DecorationSet): DecorationSet {
if (!update.startState.selection.eq(update.state.selection)) {
return super.createDeco(update.view);
} else {
return super.updateDeco(update, deco);
}
return super.updateDeco(update, deco);
}
}

View File

@@ -26,7 +26,8 @@
}
&:not(.cm-focused) {
.cm-cursor, .cm-fat-cursor {
.cm-cursor,
.cm-fat-cursor {
@apply hidden;
}
}
@@ -77,7 +78,6 @@
.cm-gutterElement {
@apply cursor-default;
}
}
.cm-gutter-lint {
@@ -91,7 +91,7 @@
@apply cursor-default opacity-80 hover:opacity-100 transition-opacity;
@apply rounded-full w-[0.9em] h-[0.9em];
content: '';
content: "";
&.cm-lint-marker-error {
@apply bg-danger;
@@ -168,7 +168,8 @@
@apply outline-text;
@apply bg-text !important;
&, * {
&,
* {
@apply text-surface font-semibold !important;
}
}
@@ -199,8 +200,7 @@
}
.cm-editor .fold-gutter-icon::after {
@apply block w-1.5 h-1.5 p-0.5 border-transparent
border-l border-b border-l-[currentColor] border-b-[currentColor] content-[''];
@apply block w-1.5 h-1.5 p-0.5 border-transparent border-l border-b border-l-[currentColor] border-b-[currentColor] content-[''];
}
/* Rotate the fold gutter chevron when open */
@@ -270,7 +270,8 @@
/* Style the tooltip for popping up "open in browser" and other stuff */
a, button {
a,
button {
@apply text-text hover:bg-surface-highlight w-full h-sm flex items-center px-2 rounded;
}
@@ -279,7 +280,7 @@
&::after {
@apply text-text bg-text h-3 w-3 ml-1;
content: '';
content: "";
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='black' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z'/%3E%3Cpath fill-rule='evenodd' d='M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z'/%3E%3C/svg%3E");
-webkit-mask-size: contain;
display: inline-block;
@@ -300,59 +301,59 @@
@apply opacity-80 italic;
&::after {
content: 'a' !important; /* Default (eg. for GraphQL) */
content: "a" !important; /* Default (eg. for GraphQL) */
}
&.cm-completionIcon-function::after {
content: 'f' !important;
content: "f" !important;
@apply text-info;
}
&.cm-completionIcon-variable::after {
content: 'x' !important;
content: "x" !important;
@apply text-primary;
}
&.cm-completionIcon-namespace::after {
content: 'n' !important;
content: "n" !important;
@apply text-warning;
}
&.cm-completionIcon-constant::after {
content: 'c' !important;
content: "c" !important;
@apply text-notice;
}
&.cm-completionIcon-class::after {
content: 'o' !important;
content: "o" !important;
}
&.cm-completionIcon-enum::after {
content: 'e' !important;
content: "e" !important;
}
&.cm-completionIcon-interface::after {
content: 'i' !important;
content: "i" !important;
}
&.cm-completionIcon-keyword::after {
content: 'k' !important;
content: "k" !important;
}
&.cm-completionIcon-method::after {
content: 'm' !important;
content: "m" !important;
}
&.cm-completionIcon-property::after {
content: 'a' !important;
content: "a" !important;
}
&.cm-completionIcon-text::after {
content: 't' !important;
content: "t" !important;
}
&.cm-completionIcon-type::after {
content: 't' !important;
content: "t" !important;
}
}
@@ -406,7 +407,7 @@
@apply appearance-none bg-none cursor-default;
}
button[name='close'] {
button[name="close"] {
@apply text-text-subtle hocus:text-text px-2 -mr-1.5 !important;
}
@@ -421,7 +422,7 @@
/* Hide the "All" button */
button[name='select'] {
button[name="select"] {
@apply hidden;
}
}

View File

@@ -28,7 +28,6 @@ import {
import { activeEnvironmentAtom } from '../../../hooks/useActiveEnvironment';
import type { WrappedEnvironmentVariable } from '../../../hooks/useEnvironmentVariables';
import { useEnvironmentVariables } from '../../../hooks/useEnvironmentVariables';
import { useRandomKey } from '../../../hooks/useRandomKey';
import { useRequestEditor } from '../../../hooks/useRequestEditor';
import { useTemplateFunctionCompletionOptions } from '../../../hooks/useTemplateFunctions';
import { editEnvironment } from '../../../lib/editEnvironment';
@@ -113,7 +112,7 @@ export function Editor({
disabled,
extraExtensions,
forcedEnvironmentId,
forceUpdateKey: forceUpdateKeyFromAbove,
forceUpdateKey,
format,
heightMode,
hideGutter,
@@ -144,10 +143,6 @@ export function Editor({
? allEnvironmentVariables.filter(autocompleteVariables)
: allEnvironmentVariables;
}, [allEnvironmentVariables, autocompleteVariables]);
// Track a local key for updates. If the default value is changed when the input is not in focus,
// regenerate this to force the field to update.
const [focusedUpdateKey, regenerateFocusedUpdateKey] = useRandomKey('initial');
const forceUpdateKey = `${forceUpdateKeyFromAbove}::${focusedUpdateKey}`;
if (settings && wrapLines === undefined) {
wrapLines = settings.editorSoftWrap;
@@ -223,7 +218,7 @@ export function Editor({
const effects = placeholderCompartment.current.reconfigure(ext);
cm.current?.view.dispatch({ effects });
},
[placeholder, type],
[placeholder],
);
// Update vim
@@ -233,12 +228,12 @@ export function Editor({
if (cm.current === null) return;
const current = keymapCompartment.current.get(cm.current.view.state) ?? [];
// PERF: This is expensive with hundreds of editors on screen, so only do it when necessary
if (settings.editorKeymap === 'default' && current === keymapExtensions['default']) return; // Nothing to do
if (settings.editorKeymap === 'vim' && current === keymapExtensions['vim']) return; // Nothing to do
if (settings.editorKeymap === 'vscode' && current === keymapExtensions['vscode']) return; // Nothing to do
if (settings.editorKeymap === 'emacs' && current === keymapExtensions['emacs']) return; // Nothing to do
if (settings.editorKeymap === 'default' && current === keymapExtensions.default) return; // Nothing to do
if (settings.editorKeymap === 'vim' && current === keymapExtensions.vim) return; // Nothing to do
if (settings.editorKeymap === 'vscode' && current === keymapExtensions.vscode) return; // Nothing to do
if (settings.editorKeymap === 'emacs' && current === keymapExtensions.emacs) return; // Nothing to do
const ext = keymapExtensions[settings.editorKeymap] ?? keymapExtensions['default'];
const ext = keymapExtensions[settings.editorKeymap] ?? keymapExtensions.default;
const effects = keymapCompartment.current.reconfigure(ext);
cm.current.view.dispatch({ effects });
},
@@ -324,6 +319,7 @@ export function Editor({
);
// Update the language extension when the language changes
// biome-ignore lint/correctness/useExhaustiveDependencies: none
useEffect(() => {
if (cm.current === null) return;
const { view, languageCompartment } = cm.current;
@@ -355,6 +351,7 @@ export function Editor({
]);
// Initialize the editor when ref mounts
// biome-ignore lint/correctness/useExhaustiveDependencies: Only reinitialize when necessary
const initEditorRef = useCallback(
function initEditorRef(container: HTMLDivElement | null) {
if (container === null) {
@@ -384,7 +381,7 @@ export function Editor({
!disableTabIndent ? keymap.of([indentWithTab]) : emptyExtension,
),
keymapCompartment.current.of(
keymapExtensions[settings.editorKeymap] ?? keymapExtensions['default'],
keymapExtensions[settings.editorKeymap] ?? keymapExtensions.default,
),
...getExtensions({
container,
@@ -434,7 +431,6 @@ export function Editor({
console.log('Failed to initialize Codemirror', e);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[forceUpdateKey],
);
@@ -456,7 +452,7 @@ export function Editor({
updateContents(cm.current.view, defaultValue || '');
}
},
[defaultValue, readOnly, regenerateFocusedUpdateKey],
[defaultValue],
);
// Add bg classes to actions, so they appear over the text
@@ -628,7 +624,7 @@ function saveCachedEditorState(stateKey: string | null, state: EditorState | nul
// Save state in sessionStorage by removing doc and saving the hash of it instead.
// This will be checked on restore and put back in if it matches.
stateObj.docHash = md5(stateObj.doc);
delete stateObj.doc;
stateObj.doc = undefined;
try {
sessionStorage.setItem(computeFullStateKey(stateKey), JSON.stringify(stateObj));
@@ -670,7 +666,9 @@ function updateContents(view: EditorView, text: string) {
if (currentDoc === text) {
return;
} else if (text.startsWith(currentDoc)) {
}
if (text.startsWith(currentDoc)) {
// If we're just appending, append only the changes. This preserves
// things like scroll position.
view.dispatch({

View File

@@ -1,6 +1,7 @@
import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
import { autocompletion, startCompletion } from '@codemirror/autocomplete';
import { LanguageSupport, LRLanguage, syntaxTree } from '@codemirror/language';
import type { SyntaxNode } from '@lezer/common';
import { parser } from './filter';
export interface FieldDef {
@@ -40,9 +41,10 @@ function wordBefore(doc: string, pos: number): { from: number; to: number; text:
function inPhrase(ctx: CompletionContext): boolean {
// Lezer node names from your grammar: Phrase is the quoted token
let n = syntaxTree(ctx.state).resolveInner(ctx.pos, -1);
for (; n; n = n.parent!) {
let n: SyntaxNode | null = syntaxTree(ctx.state).resolveInner(ctx.pos, -1);
while (n) {
if (n.name === 'Phrase') return true;
n = n.parent;
}
return false;
}

View File

@@ -1,23 +1,27 @@
// biome-ignore-all lint: Disable for generated file
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
import {highlight} from "./highlight"
import { LRParser } from '@lezer/lr';
import { highlight } from './highlight';
export const parser = LRParser.deserialize({
version: 14,
states: "%QOVQPOOPeOPOOOVQPO'#CfOjQPO'#ChO!XQPO'#CgOOQO'#Cc'#CcOVQPO'#CaOOQO'#Ca'#CaO!oQPO'#C`O!|QPO'#C_OOQO'#C^'#C^QOQPOOPOOO'#Cp'#CpP#XOPO)C>jO#`QPO,59QO#eQPO,59ROOQO,58{,58{OVQPO'#CqOOQO'#Cq'#CqO#mQPO,58zOVQPO'#CrO#zQPO,58yPOOO-E6n-E6nOOQO1G.l1G.lOOQO'#Cm'#CmOOQO'#Ck'#CkOOQO1G.m1G.mOOQO,59],59]OOQO-E6o-E6oOOQO,59^,59^OOQO-E6p-E6p",
stateData: "$]~OiPQ~OUUOXQO]RO`TO~Oi[O~OUaXXaX]aX^[X`aXbaXcaXgaXWaX~O^_O~OUUOXQO]RO`TObaO~OcSXgSXWSX~P!^OcdOgRXWRX~Oi[O~Qh]WgO~O]hO`iO~OcSagSaWSa~P!^OcdOgRaWRa~OUbc]c~",
goto: "#hgPPhnryP!YPP!c!c!lPP!uP!xPP#U#[#bQZOR^QTYOQSXOQRmdUWOQdQ`USbWcRka_VOQUWacd_TOQUWacd_SOQUWacdRj_^TOQUWacdRi_Q]PRf]QcWRlcQeXRne",
nodeNames: "⚠ Query Expr OrExpr AndExpr Unary Not Primary RParen LParen Group Field FieldName Word Colon FieldValue Phrase Term And Or",
states:
"%QOVQPOOPeOPOOOVQPO'#CfOjQPO'#ChO!XQPO'#CgOOQO'#Cc'#CcOVQPO'#CaOOQO'#Ca'#CaO!oQPO'#C`O!|QPO'#C_OOQO'#C^'#C^QOQPOOPOOO'#Cp'#CpP#XOPO)C>jO#`QPO,59QO#eQPO,59ROOQO,58{,58{OVQPO'#CqOOQO'#Cq'#CqO#mQPO,58zOVQPO'#CrO#zQPO,58yPOOO-E6n-E6nOOQO1G.l1G.lOOQO'#Cm'#CmOOQO'#Ck'#CkOOQO1G.m1G.mOOQO,59],59]OOQO-E6o-E6oOOQO,59^,59^OOQO-E6p-E6p",
stateData:
'$]~OiPQ~OUUOXQO]RO`TO~Oi[O~OUaXXaX]aX^[X`aXbaXcaXgaXWaX~O^_O~OUUOXQO]RO`TObaO~OcSXgSXWSX~P!^OcdOgRXWRX~Oi[O~Qh]WgO~O]hO`iO~OcSagSaWSa~P!^OcdOgRaWRa~OUbc]c~',
goto: '#hgPPhnryP!YPP!c!c!lPP!uP!xPP#U#[#bQZOR^QTYOQSXOQRmdUWOQdQ`USbWcRka_VOQUWacd_TOQUWacd_SOQUWacdRj_^TOQUWacdRi_Q]PRf]QcWRlcQeXRne',
nodeNames:
'⚠ Query Expr OrExpr AndExpr Unary Not Primary RParen LParen Group Field FieldName Word Colon FieldValue Phrase Term And Or',
maxTerm: 25,
nodeProps: [
["openedBy", 8,"LParen"],
["closedBy", 9,"RParen"]
['openedBy', 8, 'LParen'],
['closedBy', 9, 'RParen'],
],
propSources: [highlight],
skippedNodes: [0,20],
skippedNodes: [0, 20],
repeatNodeCount: 3,
tokenData: ")f~RgX^!jpq!jrs#_xy${yz%Q}!O%V!Q![%[![!]%m!c!d%r!d!p%[!p!q'V!q!r(j!r!}%[#R#S%[#T#o%[#y#z!j$f$g!j#BY#BZ!j$IS$I_!j$I|$JO!j$JT$JU!j$KV$KW!j&FU&FV!j~!oYi~X^!jpq!j#y#z!j$f$g!j#BY#BZ!j$IS$I_!j$I|$JO!j$JT$JU!j$KV$KW!j&FU&FV!j~#bVOr#_rs#ws#O#_#O#P#|#P;'S#_;'S;=`$u<%lO#_~#|O`~~$PRO;'S#_;'S;=`$Y;=`O#_~$]WOr#_rs#ws#O#_#O#P#|#P;'S#_;'S;=`$u;=`<%l#_<%lO#_~$xP;=`<%l#_~%QOX~~%VOW~~%[OU~~%aS]~!Q![%[!c!}%[#R#S%[#T#o%[~%rO^~~%wU]~!Q![%[!c!p%[!p!q&Z!q!}%[#R#S%[#T#o%[~&`U]~!Q![%[!c!f%[!f!g&r!g!}%[#R#S%[#T#o%[~&ySb~]~!Q![%[!c!}%[#R#S%[#T#o%[~'[U]~!Q![%[!c!q%[!q!r'n!r!}%[#R#S%[#T#o%[~'sU]~!Q![%[!c!v%[!v!w(V!w!}%[#R#S%[#T#o%[~(^SU~]~!Q![%[!c!}%[#R#S%[#T#o%[~(oU]~!Q![%[!c!t%[!t!u)R!u!}%[#R#S%[#T#o%[~)YSc~]~!Q![%[!c!}%[#R#S%[#T#o%[",
tokenData:
")f~RgX^!jpq!jrs#_xy${yz%Q}!O%V!Q![%[![!]%m!c!d%r!d!p%[!p!q'V!q!r(j!r!}%[#R#S%[#T#o%[#y#z!j$f$g!j#BY#BZ!j$IS$I_!j$I|$JO!j$JT$JU!j$KV$KW!j&FU&FV!j~!oYi~X^!jpq!j#y#z!j$f$g!j#BY#BZ!j$IS$I_!j$I|$JO!j$JT$JU!j$KV$KW!j&FU&FV!j~#bVOr#_rs#ws#O#_#O#P#|#P;'S#_;'S;=`$u<%lO#_~#|O`~~$PRO;'S#_;'S;=`$Y;=`O#_~$]WOr#_rs#ws#O#_#O#P#|#P;'S#_;'S;=`$u;=`<%l#_<%lO#_~$xP;=`<%l#_~%QOX~~%VOW~~%[OU~~%aS]~!Q![%[!c!}%[#R#S%[#T#o%[~%rO^~~%wU]~!Q![%[!c!p%[!p!q&Z!q!}%[#R#S%[#T#o%[~&`U]~!Q![%[!c!f%[!f!g&r!g!}%[#R#S%[#T#o%[~&ySb~]~!Q![%[!c!}%[#R#S%[#T#o%[~'[U]~!Q![%[!c!q%[!q!r'n!r!}%[#R#S%[#T#o%[~'sU]~!Q![%[!c!v%[!v!w(V!w!}%[#R#S%[#T#o%[~(^SU~]~!Q![%[!c!}%[#R#S%[#T#o%[~(oU]~!Q![%[!c!t%[!t!u)R!u!}%[#R#S%[#T#o%[~)YSc~]~!Q![%[!c!}%[#R#S%[#T#o%[",
tokenizers: [0],
topRules: {"Query":[0,1]},
tokenPrec: 145
})
topRules: { Query: [0, 1] },
tokenPrec: 145,
});

View File

@@ -255,9 +255,9 @@ type Technique = 'substring' | 'fuzzy' | 'strict';
function includes(hay: string | undefined, needle: string, technique: Technique): boolean {
if (!hay || !needle) return false;
else if (technique === 'strict') return hay === needle;
else if (technique === 'fuzzy') return !!fuzzyMatch(hay, needle);
else return hay.indexOf(needle) !== -1;
if (technique === 'strict') return hay === needle;
if (technique === 'fuzzy') return !!fuzzyMatch(hay, needle);
return hay.indexOf(needle) !== -1;
}
export function evaluate(ast: Ast | null, doc: Doc): boolean {

View File

@@ -11,9 +11,10 @@ const REGEX =
const tooltip = hoverTooltip(
(view, pos, side) => {
const { from, text } = view.state.doc.lineAt(pos);
let match;
let match: RegExpExecArray | null;
let found: { start: number; end: number } | null = null;
// biome-ignore lint/suspicious/noAssignInExpressions: none
while ((match = REGEX.exec(text))) {
const start = from + match.index;
const end = start + match[0].length;
@@ -28,7 +29,7 @@ const tooltip = hoverTooltip(
return null;
}
if ((found.start == pos && side < 0) || (found.end == pos && side > 0)) {
if ((found.start === pos && side < 0) || (found.end === pos && side > 0)) {
return null;
}
@@ -37,7 +38,7 @@ const tooltip = hoverTooltip(
end: found.end,
create() {
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
const link = text.substring(found!.start - from, found!.end - from);
const link = text.substring(found?.start - from, found?.end - from);
const dom = document.createElement('div');
const $open = document.createElement('a');
@@ -77,7 +78,7 @@ const tooltip = hoverTooltip(
},
);
const decorator = function () {
const decorator = () => {
const placeholderMatcher = new MatchDecorator({
regexp: REGEX,
decoration(match, view, matchStartPos) {

View File

@@ -12,7 +12,7 @@ export function jsonParseLinter() {
// syntax with repeating `1` characters, so it's valid JSON and the position is still correct.
const escapedDoc = doc.replace(TEMPLATE_SYNTAX_REGEX, '1');
jsonLintParse(escapedDoc);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: none
} catch (err: any) {
if (!('location' in err)) {
return [];

View File

@@ -1,6 +1,5 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
pairs = 1,
export const pairs = 1,
Key = 2,
Sep = 3,
Value = 4
Value = 4;

View File

@@ -1,19 +1,29 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
import {highlight} from "./highlight"
import { LRParser } from '@lezer/lr';
import { highlight } from './highlight';
export const parser = LRParser.deserialize({
version: 14,
states: "zQQOPOOOVOQO'#CaQQOPOOO[OSO,58{OOOO-E6_-E6_OaOQO1G.gOOOO7+$R7+$R",
stateData: "f~OQPO~ORRO~OSTO~OVUO~O",
goto: "]UPPPPPVQQORSQ",
nodeNames: "⚠ pairs Key Sep Value",
stateData: 'f~OQPO~ORRO~OSTO~OVUO~O',
goto: ']UPPPPPVQQORSQ',
nodeNames: '⚠ pairs Key Sep Value',
maxTerm: 7,
propSources: [highlight],
skippedNodes: [0],
repeatNodeCount: 1,
tokenData: "$]VRVOYhYZ#[Z![h![!]#o!];'Sh;'S;=`#U<%lOhToVQPSSOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOhP!ZSQPO![!U!];'S!U;'S;=`!g<%lO!UP!jP;=`<%l!US!rSSSOY!mZ;'S!m;'S;=`#O<%lO!mS#RP;=`<%l!mT#XP;=`<%lhR#cSVQQPO![!U!];'S!U;'S;=`!g<%lO!UV#vVRQSSOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOh",
tokenData:
"$]VRVOYhYZ#[Z![h![!]#o!];'Sh;'S;=`#U<%lOhToVQPSSOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOhP!ZSQPO![!U!];'S!U;'S;=`!g<%lO!UP!jP;=`<%l!US!rSSSOY!mZ;'S!m;'S;=`#O<%lO!mS#RP;=`<%l!mT#XP;=`<%lhR#cSVQQPO![!U!];'S!U;'S;=`!g<%lO!UV#vVRQSSOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOh",
tokenizers: [0, 1, 2],
topRules: {"pairs":[0,1]},
topRules: { pairs: [0, 1] },
tokenPrec: 0,
termNames: {"0":"⚠","1":"@top","2":"Key","3":"Sep","4":"Value","5":"(Key Sep Value \"\\n\")+","6":"␄","7":"\"\\n\""}
})
termNames: {
'0': '⚠',
'1': '@top',
'2': 'Key',
'3': 'Sep',
'4': 'Value',
'5': '(Key Sep Value "\\n")+',
'6': '␄',
'7': '"\\n"',
},
});

View File

@@ -1,4 +1,3 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
Template = 1,
Text = 2
export const Template = 1,
Text = 2;

View File

@@ -1,16 +1,16 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
import { LRParser } from '@lezer/lr';
export const parser = LRParser.deserialize({
version: 14,
states: "[OQOPOOQOOOOO",
stateData: "V~OQPO~O",
goto: "QPP",
nodeNames: "⚠ Template Text",
states: '[OQOPOOQOOOOO',
stateData: 'V~OQPO~O',
goto: 'QPP',
nodeNames: '⚠ Template Text',
maxTerm: 3,
skippedNodes: [0],
repeatNodeCount: 0,
tokenData: "p~RRO;'S[;'S;=`j<%lO[~aRQ~O;'S[;'S;=`j<%lO[~mP;=`<%l[",
tokenizers: [0],
topRules: {"Template":[0,1]},
tokenPrec: 0
})
topRules: { Template: [0, 1] },
tokenPrec: 0,
});

View File

@@ -44,14 +44,13 @@ export function twigCompletion({ options }: TwigCompletionConfig) {
if (toMatch === null) return null;
const matchLen = toMatch.to - toMatch.from;
if (toMatch.from >0 && matchLen < MIN_MATCH_NAME) {
if (toMatch.from > 0 && matchLen < MIN_MATCH_NAME) {
return null;
}
const completions: Completion[] = options
.flatMap((o): Completion[] => {
const matchSegments = toMatch!.text.replace(/^\$/, '').split('.');
const matchSegments = toMatch.text.replace(/^\$/, '').split('.');
const optionSegments = o.name.split('.');
// If not on the last segment, only complete the namespace
@@ -59,7 +58,7 @@ export function twigCompletion({ options }: TwigCompletionConfig) {
const prefix = optionSegments.slice(0, matchSegments.length).join('.');
return [
{
label: prefix + '.*',
label: `${prefix}.*`,
type: 'namespace',
detail: 'namespace',
apply: (view, _completion, from, to) => {

View File

@@ -1,7 +1,7 @@
import { syntaxTree } from '@codemirror/language';
import type { Range } from '@codemirror/state';
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
import { Decoration, ViewPlugin, WidgetType, EditorView } from '@codemirror/view';
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
class PathPlaceholderWidget extends WidgetType {
readonly #clickListenerCallback: () => void;
@@ -23,7 +23,7 @@ class PathPlaceholderWidget extends WidgetType {
toDOM() {
const elt = document.createElement('span');
elt.className = `x-theme-templateTag x-theme-templateTag--secondary template-tag`;
elt.className = 'x-theme-templateTag x-theme-templateTag--secondary template-tag';
elt.textContent = this.rawText;
elt.addEventListener('click', this.#clickListenerCallback);
return elt;

View File

@@ -1,7 +1,7 @@
import { syntaxTree } from '@codemirror/language';
import type { Range } from '@codemirror/state';
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
import { Decoration, ViewPlugin, WidgetType, EditorView } from '@codemirror/view';
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
import type { SyntaxNodeRef } from '@lezer/common';
import { parseTemplate } from '@yaakapp-internal/templates';
import type { TwigCompletionOption } from './completion';

View File

@@ -1,8 +1,7 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
Template = 1,
export const Template = 1,
Tag = 2,
TagOpen = 3,
TagContent = 4,
TagClose = 5,
Text = 6
Text = 6;

View File

@@ -1,18 +1,20 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser, LocalTokenGroup} from "@lezer/lr"
import {highlight} from "./highlight"
import { LocalTokenGroup, LRParser } from '@lezer/lr';
import { highlight } from './highlight';
export const parser = LRParser.deserialize({
version: 14,
states: "!^QQOPOOOOOO'#C_'#C_OYOQO'#C^OOOO'#Cc'#CcQQOPOOOOOO'#Cd'#CdO_OQO,58xOOOO-E6a-E6aOOOO-E6b-E6bOOOO1G.d1G.d",
stateData: "g~OUROYPO~OSTO~OSTOTXO~O",
goto: "nXPPY^PPPbhTROSTQOSQSORVSQUQRWU",
nodeNames: "⚠ Template Tag TagOpen TagContent TagClose Text",
states:
"!^QQOPOOOOOO'#C_'#C_OYOQO'#C^OOOO'#Cc'#CcQQOPOOOOOO'#Cd'#CdO_OQO,58xOOOO-E6a-E6aOOOO-E6b-E6bOOOO1G.d1G.d",
stateData: 'g~OUROYPO~OSTO~OSTOTXO~O',
goto: 'nXPPY^PPPbhTROSTQOSQSORVSQUQRWU',
nodeNames: '⚠ Template Tag TagOpen TagContent TagClose Text',
maxTerm: 10,
propSources: [highlight],
skippedNodes: [0],
repeatNodeCount: 2,
tokenData: "#]~RTOtbtu!hu;'Sb;'S;=`!]<%lOb~gTU~Otbtuvu;'Sb;'S;=`!]<%lOb~yUO#ob#p;'Sb;'S;=`!]<%l~b~Ob~~!c~!`P;=`<%lb~!hOU~~!kVO#ob#o#p#Q#p;'Sb;'S;=`!]<%l~b~Ob~~!c~#TP!}#O#W~#]OY~",
tokenizers: [1, new LocalTokenGroup("b~RP#P#QU~XP#q#r[~aOT~~", 17, 4)],
topRules: {"Template":[0,1]},
tokenPrec: 0
})
tokenData:
"#]~RTOtbtu!hu;'Sb;'S;=`!]<%lOb~gTU~Otbtuvu;'Sb;'S;=`!]<%lOb~yUO#ob#p;'Sb;'S;=`!]<%l~b~Ob~~!c~!`P;=`<%lb~!hOU~~!kVO#ob#o#p#Q#p;'Sb;'S;=`!]<%l~b~Ob~~!c~#TP!}#O#W~#]OY~",
tokenizers: [1, new LocalTokenGroup('b~RP#P#QU~XP#q#r[~aOT~~', 17, 4)],
topRules: { Template: [0, 1] },
tokenPrec: 0,
});

View File

@@ -1,10 +1,9 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
url = 1,
export const url = 1,
Protocol = 2,
Host = 3,
Port = 4,
Path = 5,
Placeholder = 6,
PathSegment = 7,
Query = 8
Query = 8;

View File

@@ -1,19 +1,20 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
import {highlight} from "./highlight"
import { LRParser } from '@lezer/lr';
import { highlight } from './highlight';
export const parser = LRParser.deserialize({
version: 14,
states: "!|OQOPOOQYOPOOOTOPOOObOQO'#CdOjOPO'#C`OuOSO'#CcQOOOOOQ]OPOOOOOO,59O,59OOOOO-E6b-E6bOzOPO,58}O!SOSO'#CeO!XOPO1G.iOOOO,59P,59POOOO-E6c-E6c",
stateData: "!g~OQQORPO~OZRO[TO~OTWOUWO~OZROYSX[SX~O]YO~O^ZOYVa~O]]O~O^ZOYVi~OQRTUT~",
goto: "nYPPPPZPP^bhRVPTUPVQSPRXSQ[YR^[",
nodeNames: "⚠ url Protocol Host Path Placeholder PathSegment Query",
states:
"!|OQOPOOQYOPOOOTOPOOObOQO'#CdOjOPO'#C`OuOSO'#CcQOOOOOQ]OPOOOOOO,59O,59OOOOO-E6b-E6bOzOPO,58}O!SOSO'#CeO!XOPO1G.iOOOO,59P,59POOOO-E6c-E6c",
stateData: '!g~OQQORPO~OZRO[TO~OTWOUWO~OZROYSX[SX~O]YO~O^ZOYVa~O]]O~O^ZOYVi~OQRTUT~',
goto: 'nYPPPPZPP^bhRVPTUPVQSPRXSQ[YR^[',
nodeNames: '⚠ url Protocol Host Path Placeholder PathSegment Query',
maxTerm: 14,
propSources: [highlight],
skippedNodes: [0],
repeatNodeCount: 2,
tokenData: ".i~RgOs!jtv!jvw#Xw}!j}!O#r!O!P#r!P!Q%U!Q![%Z![!]'o!]!a!j!a!b+W!b!c!j!c!}+]!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o+]#o;'S!j;'S;=`#R<%lO!jQ!oUUQOs!jt!P!j!Q!a!j!b;'S!j;'S;=`#R<%lO!jQ#UP;=`<%l!jR#`U^PUQOs!jt!P!j!Q!a!j!b;'S!j;'S;=`#R<%lO!jR#ycRPUQOs!jt}!j}!O#r!O!P#r!Q![#r![!]#r!]!a!j!b!c!j!c!}#r!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o#r#o;'S!j;'S;=`#R<%lO!j~%ZOZ~V%de]SRPUQOs!jt}!j}!O#r!O!P#r!Q![%Z![!]#r!]!_!j!_!`&u!`!a!j!b!c!j!c!}%Z!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o%Z#o;'S!j;'S;=`#R<%lO!jU&|Z]SUQOs!jt!P!j!Q![&u![!a!j!b!c!j!c!}&u!}#T!j#T#o&u#o;'S!j;'S;=`#R<%lO!jR'vcRPUQOs)Rt})R}!O)r!O!P)r!Q![)r![!])r!]!a)R!b!c)R!c!})r!}#O)r#O#P)R#P#Q)r#Q#R)R#R#S)r#S#T)R#T#o)r#o;'S)R;'S;=`)l<%lO)RQ)YUTQUQOs)Rt!P)R!Q!a)R!b;'S)R;'S;=`)l<%lO)RQ)oP;=`<%l)RR){cRPTQUQOs)Rt})R}!O)r!O!P)r!Q![)r![!])r!]!a)R!b!c)R!c!})r!}#O)r#O#P)R#P#Q)r#Q#R)R#R#S)r#S#T)R#T#o)r#o;'S)R;'S;=`)l<%lO)R~+]O[~V+fe]SRPUQOs!jt}!j}!O#r!O!P#r!Q![%Z![!],w!]!_!j!_!`&u!`!a!j!b!c!j!c!}+]!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o+]#o;'S!j;'S;=`#R<%lO!jR-OdRPUQOs!jt}!j}!O#r!O!P#r!P!Q.^!Q![#r![!]#r!]!a!j!b!c!j!c!}#r!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o#r#o;'S!j;'S;=`#R<%lO!jP.aP!P!Q.dP.iOQP",
tokenData:
".i~RgOs!jtv!jvw#Xw}!j}!O#r!O!P#r!P!Q%U!Q![%Z![!]'o!]!a!j!a!b+W!b!c!j!c!}+]!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o+]#o;'S!j;'S;=`#R<%lO!jQ!oUUQOs!jt!P!j!Q!a!j!b;'S!j;'S;=`#R<%lO!jQ#UP;=`<%l!jR#`U^PUQOs!jt!P!j!Q!a!j!b;'S!j;'S;=`#R<%lO!jR#ycRPUQOs!jt}!j}!O#r!O!P#r!Q![#r![!]#r!]!a!j!b!c!j!c!}#r!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o#r#o;'S!j;'S;=`#R<%lO!j~%ZOZ~V%de]SRPUQOs!jt}!j}!O#r!O!P#r!Q![%Z![!]#r!]!_!j!_!`&u!`!a!j!b!c!j!c!}%Z!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o%Z#o;'S!j;'S;=`#R<%lO!jU&|Z]SUQOs!jt!P!j!Q![&u![!a!j!b!c!j!c!}&u!}#T!j#T#o&u#o;'S!j;'S;=`#R<%lO!jR'vcRPUQOs)Rt})R}!O)r!O!P)r!Q![)r![!])r!]!a)R!b!c)R!c!})r!}#O)r#O#P)R#P#Q)r#Q#R)R#R#S)r#S#T)R#T#o)r#o;'S)R;'S;=`)l<%lO)RQ)YUTQUQOs)Rt!P)R!Q!a)R!b;'S)R;'S;=`)l<%lO)RQ)oP;=`<%l)RR){cRPTQUQOs)Rt})R}!O)r!O!P)r!Q![)r![!])r!]!a)R!b!c)R!c!})r!}#O)r#O#P)R#P#Q)r#Q#R)R#R#S)r#S#T)R#T#o)r#o;'S)R;'S;=`)l<%lO)R~+]O[~V+fe]SRPUQOs!jt}!j}!O#r!O!P#r!Q![%Z![!],w!]!_!j!_!`&u!`!a!j!b!c!j!c!}+]!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o+]#o;'S!j;'S;=`#R<%lO!jR-OdRPUQOs!jt}!j}!O#r!O!P#r!P!Q.^!Q![#r![!]#r!]!a!j!b!c!j!c!}#r!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o#r#o;'S!j;'S;=`#R<%lO!jP.aP!P!Q.dP.iOQP",
tokenizers: [0, 1, 2],
topRules: {"url":[0,1]},
tokenPrec: 63
})
topRules: { url: [0, 1] },
tokenPrec: 63,
});

View File

@@ -24,6 +24,7 @@ export function HotKey({ action, className, variant }: Props) {
)}
>
{labelParts.map((char, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: none
<div key={index} className="min-w-[1.1em] text-center">
{char}
</div>

View File

@@ -1,5 +1,6 @@
import classNames from 'classnames';
import React, { Fragment } from 'react';
import type React from 'react';
import { Fragment } from 'react';
import type { HotkeyAction } from '../../hooks/useHotKey';
import { HotKey } from './HotKey';
import { HotKeyLabel } from './HotKeyLabel';

View File

@@ -14,10 +14,10 @@ export function HttpResponseDurationTag({ response }: Props) {
clearInterval(timeout.current);
if (response.state === 'closed') return;
timeout.current = setInterval(() => {
setFallbackElapsed(Date.now() - new Date(response.createdAt + 'Z').getTime());
setFallbackElapsed(Date.now() - new Date(`${response.createdAt}Z`).getTime());
}, 100);
return () => clearInterval(timeout.current);
}, [response.createdAt, response.elapsed, response.state]);
}, [response.createdAt, response.state]);
const title = `HEADER: ${formatMillis(response.elapsedHeaders)}\nTOTAL: ${formatMillis(response.elapsed)}`;
@@ -33,12 +33,12 @@ export function HttpResponseDurationTag({ response }: Props) {
function formatMillis(ms: number) {
if (ms < 1000) {
return `${ms} ms`;
} else if (ms < 60_000) {
}
if (ms < 60_000) {
const seconds = (ms / 1000).toFixed(ms < 10_000 ? 1 : 0);
return `${seconds} s`;
} else {
const minutes = Math.floor(ms / 60_000);
const seconds = Math.round((ms % 60_000) / 1000);
return `${minutes}m ${seconds}s`;
}
const minutes = Math.floor(ms / 60_000);
const seconds = Math.round((ms % 60_000) / 1000);
return `${minutes}m ${seconds}s`;
}

View File

@@ -11,7 +11,7 @@ interface Props {
export function HttpStatusTag({ response, className, showReason, short }: Props) {
const { status, state } = response;
let colorClass;
let colorClass: string;
let label = `${status}`;
if (state === 'initialized') {

View File

@@ -33,6 +33,7 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
showBadge,
iconColor,
isLoading,
type = 'button',
...props
}: IconButtonProps,
ref,
@@ -56,6 +57,7 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
innerClassName="flex items-center justify-center"
size={size}
color={color}
type={type}
className={classNames(
className,
'group/button relative flex-shrink-0',

View File

@@ -1,4 +1,3 @@
import React from 'react';
import type { IconProps } from './Icon';
import { Icon } from './Icon';
import type { TooltipProps } from './Tooltip';

View File

@@ -88,9 +88,8 @@ export function Input({ type, ...props }: InputProps) {
// use the encrypted input component.
if (type === 'password' && props.autocompleteFunctions) {
return <EncryptionInput {...props} />;
} else {
return <BaseInput type={type} {...props} />;
}
return <BaseInput type={type} {...props} />;
}
function BaseInput({
@@ -145,7 +144,7 @@ function BaseInput({
isFocused: () => editorRef.current?.hasFocus ?? false,
value: () => editorRef.current?.state.doc.toString() ?? '',
dispatch: (...args) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: none
editorRef.current?.dispatch(...(args as any));
},
selectAll() {
@@ -168,7 +167,9 @@ function BaseInput({
);
useEffect(() => {
const fn = () => (skipNextFocus.current = true);
const fn = () => {
skipNextFocus.current = true;
};
window.addEventListener('focus', fn);
return () => {
window.removeEventListener('focus', fn);

View File

@@ -5,7 +5,7 @@ import { Icon } from './Icon';
interface Props {
depth?: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: none
attrValue: any;
attrKey?: string | number;
attrKeyJsonPath?: string;
@@ -47,15 +47,17 @@ export const JsonAttributeTree = ({
))
: null,
isExpandable: Object.keys(attrValue).length > 0,
label: isExpanded ? `{${Object.keys(attrValue).length || ' '}}` : `{⋯}`,
label: isExpanded ? `{${Object.keys(attrValue).length || ' '}}` : '{⋯}',
labelClassName: 'text-text-subtlest',
};
} else if (jsonType === '[object Array]') {
}
if (jsonType === '[object Array]') {
return {
children: isExpanded
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
? // biome-ignore lint/suspicious/noExplicitAny: none
attrValue.flatMap((v: any, i: number) => (
<JsonAttributeTree
// biome-ignore lint/suspicious/noArrayIndexKey: none
key={i}
depth={depth + 1}
attrValue={v}
@@ -65,26 +67,27 @@ export const JsonAttributeTree = ({
))
: null,
isExpandable: attrValue.length > 0,
label: isExpanded ? `[${attrValue.length || ' '}]` : `[⋯]`,
label: isExpanded ? `[${attrValue.length || ' '}]` : '[⋯]',
labelClassName: 'text-text-subtlest',
};
} else {
return {
children: null,
isExpandable: false,
label: jsonType === '[object String]' ? `"${attrValue}"` : `${attrValue}`,
labelClassName: classNames(
jsonType === '[object Boolean]' && 'text-primary',
jsonType === '[object Number]' && 'text-info',
jsonType === '[object String]' && 'text-notice',
jsonType === '[object Null]' && 'text-danger',
),
};
}
return {
children: null,
isExpandable: false,
label: jsonType === '[object String]' ? `"${attrValue}"` : `${attrValue}`,
labelClassName: classNames(
jsonType === '[object Boolean]' && 'text-primary',
jsonType === '[object Number]' && 'text-info',
jsonType === '[object String]' && 'text-notice',
jsonType === '[object Null]' && 'text-danger',
),
};
}, [attrValue, attrKeyJsonPath, isExpanded, depth]);
const labelEl = (
<span className={classNames(labelClassName, 'cursor-text select-text group-hover:text-text-subtle')}>
<span
className={classNames(labelClassName, 'cursor-text select-text group-hover:text-text-subtle')}
>
{label}
</span>
);
@@ -98,7 +101,11 @@ export const JsonAttributeTree = ({
>
<div className="flex items-center">
{isExpandable ? (
<button className="group relative flex items-center pl-4 w-full" onClick={toggleExpanded}>
<button
type="button"
className="group relative flex items-center pl-4 w-full"
onClick={toggleExpanded}
>
<Icon
size="xs"
icon="chevron_right"
@@ -131,7 +138,7 @@ function joinObjectKey(baseKey: string | undefined, key: string): string {
const quotedKey = key.match(/^[a-z0-9_]+$/i) ? key : `\`${key}\``;
if (baseKey == null) return quotedKey;
else return `${baseKey}.${quotedKey}`;
return `${baseKey}.${quotedKey}`;
}
function joinArrayKey(baseKey: string | undefined, index: number): string {

View File

@@ -13,6 +13,7 @@ export function KeyValueRows({ children }: Props) {
<table className="text-xs font-mono min-w-0 w-full mb-auto">
<tbody className="divide-y divide-surface-highlight">
{children.map((child, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: none
<tr key={i}>{child}</tr>
))}
</tbody>

View File

@@ -37,6 +37,7 @@ export function Label({
{required === true && <span className="text-text-subtlest">*</span>}
</span>
{tags.map((tag, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: none
<span key={i} className="text-xs text-text-subtlest">
({tag})
</span>

Some files were not shown because too many files have changed in this diff Show More