mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-24 10:21:37 +01:00
Switch to BiomeJS (#306)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import type { IconProps } from './Icon';
|
||||
import { Icon } from './Icon';
|
||||
import type { TooltipProps } from './Tooltip';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -143,6 +143,7 @@ export function PairEditor({
|
||||
[handle, pairs, setRef],
|
||||
);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: Only care about forceUpdateKey
|
||||
useEffect(() => {
|
||||
// Remove empty headers on initial render and ensure they all have valid ids (pairs didn't use to have IDs)
|
||||
const newPairs: PairWithId[] = [];
|
||||
@@ -161,8 +162,6 @@ export function PairEditor({
|
||||
|
||||
setPairs(newPairs);
|
||||
regenerateLocalForceUpdateKey();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [forceUpdateKey]);
|
||||
|
||||
const setPairsAndSave = useCallback(
|
||||
@@ -534,9 +533,8 @@ export function PairEditorRow({
|
||||
const valueAutocompleteVariablesFiltered = useMemo<EditorProps['autocompleteVariables']>(() => {
|
||||
if (valueAutocompleteVariables === 'environment') {
|
||||
return (v: WrappedEnvironmentVariable): boolean => v.variable.name !== pair.name;
|
||||
} else {
|
||||
return valueAutocompleteVariables;
|
||||
}
|
||||
return valueAutocompleteVariables;
|
||||
}, [pair.name, valueAutocompleteVariables]);
|
||||
|
||||
const handleSetRef = useCallback(
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { Pair, PairWithId } from './PairEditor';
|
||||
export function ensurePairId(p: Pair): PairWithId {
|
||||
if (typeof p.id === 'string') {
|
||||
return p as PairWithId;
|
||||
} else {
|
||||
return { ...p, id: p.id ?? generateId() };
|
||||
}
|
||||
return { ...p, id: p.id ?? generateId() };
|
||||
}
|
||||
|
||||
@@ -194,6 +194,8 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
||||
key={forceUpdateKey}
|
||||
type={type === 'password' && !obscured ? 'text' : type}
|
||||
name={name}
|
||||
// biome-ignore lint/a11y/noAutofocus: Who cares
|
||||
autoFocus={autoFocus}
|
||||
defaultValue={defaultValue ?? undefined}
|
||||
autoComplete="off"
|
||||
autoCapitalize="off"
|
||||
@@ -204,7 +206,6 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
required={required}
|
||||
autoFocus={autoFocus}
|
||||
placeholder={placeholder}
|
||||
onKeyDownCapture={onKeyDownCapture}
|
||||
/>
|
||||
|
||||
@@ -30,7 +30,7 @@ export function Prompt({
|
||||
[onResult, value],
|
||||
);
|
||||
|
||||
const id = 'prompt.form.' + useRef(generateId()).current;
|
||||
const id = `prompt.form.${useRef(generateId()).current}`;
|
||||
|
||||
return (
|
||||
<form
|
||||
|
||||
@@ -45,15 +45,14 @@ export function RadioDropdown<T = string | null>({
|
||||
...items.map((item) => {
|
||||
if (item.type === 'separator') {
|
||||
return item;
|
||||
} else {
|
||||
return {
|
||||
key: item.value,
|
||||
label: item.label,
|
||||
rightSlot: item.rightSlot,
|
||||
onSelect: () => onChange(item.value),
|
||||
leftSlot: <Icon icon={value === item.value ? 'check' : 'empty'} />,
|
||||
} as DropdownItem;
|
||||
}
|
||||
return {
|
||||
key: item.value,
|
||||
label: item.label,
|
||||
rightSlot: item.rightSlot,
|
||||
onSelect: () => onChange(item.value),
|
||||
leftSlot: <Icon icon={value === item.value ? 'check' : 'empty'} />,
|
||||
} as DropdownItem;
|
||||
}),
|
||||
...((itemsAfter
|
||||
? [{ type: 'separator', hidden: itemsAfter[0]?.type === 'separator' }, ...itemsAfter]
|
||||
|
||||
@@ -37,18 +37,18 @@ export function SegmentedControl<T extends string>({
|
||||
const selectedIndex = options.findIndex((o) => o.value === selectedValue);
|
||||
if (e.key === 'ArrowRight') {
|
||||
const newIndex = Math.abs((selectedIndex + 1) % options.length);
|
||||
setSelectedValue(options[newIndex]!.value);
|
||||
options[newIndex] && setSelectedValue(options[newIndex].value);
|
||||
const child = containerRef.current?.children[newIndex] as HTMLButtonElement;
|
||||
child.focus();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
const newIndex = Math.abs((selectedIndex - 1) % options.length);
|
||||
setSelectedValue(options[newIndex]!.value);
|
||||
options[newIndex] && setSelectedValue(options[newIndex].value);
|
||||
const child = containerRef.current?.children[newIndex] as HTMLButtonElement;
|
||||
child.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{options.map((o, i) => {
|
||||
{options.map((o) => {
|
||||
const isSelected = selectedValue === o.value;
|
||||
const isActive = value === o.value;
|
||||
return (
|
||||
@@ -63,7 +63,7 @@ export function SegmentedControl<T extends string>({
|
||||
'!px-1.5 !w-auto',
|
||||
'focus:ring-border-focus',
|
||||
)}
|
||||
key={i}
|
||||
key={o.label}
|
||||
title={o.label}
|
||||
icon={o.icon}
|
||||
onClick={() => onChange(o.value)}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
@@ -7,7 +8,6 @@ import { Label } from './Label';
|
||||
import type { RadioDropdownItem } from './RadioDropdown';
|
||||
import { RadioDropdown } from './RadioDropdown';
|
||||
import { HStack } from './Stacks';
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
|
||||
export interface SelectProps<T extends string> {
|
||||
name: string;
|
||||
|
||||
@@ -10,9 +10,15 @@ interface Props {
|
||||
color?: Color;
|
||||
}
|
||||
|
||||
export function Separator({ color, className, dashed, orientation = 'horizontal', children }: Props) {
|
||||
export function Separator({
|
||||
color,
|
||||
className,
|
||||
dashed,
|
||||
orientation = 'horizontal',
|
||||
children,
|
||||
}: Props) {
|
||||
return (
|
||||
<div role="separator" className={classNames(className, 'flex items-center w-full')}>
|
||||
<div role="presentation" className={classNames(className, 'flex items-center w-full')}>
|
||||
{children && (
|
||||
<div className="text-sm text-text-subtlest mr-2 whitespace-nowrap">{children}</div>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
|
||||
import { useContainerSize } from '../../hooks/useContainerQuery';
|
||||
@@ -99,8 +99,10 @@ export function SplitLayout({
|
||||
containerRef.current,
|
||||
);
|
||||
const $c = containerRef.current;
|
||||
const containerWidth = $c.clientWidth - parseFloat(paddingLeft) - parseFloat(paddingRight);
|
||||
const containerHeight = $c.clientHeight - parseFloat(paddingTop) - parseFloat(paddingBottom);
|
||||
const containerWidth =
|
||||
$c.clientWidth - Number.parseFloat(paddingLeft) - Number.parseFloat(paddingRight);
|
||||
const containerHeight =
|
||||
$c.clientHeight - Number.parseFloat(paddingTop) - Number.parseFloat(paddingBottom);
|
||||
|
||||
const mouseStartX = e.xStart;
|
||||
const mouseStartY = e.yStart;
|
||||
|
||||
@@ -20,7 +20,7 @@ interface HStackProps extends BaseStackProps {
|
||||
|
||||
export const HStack = forwardRef(function HStack(
|
||||
{ className, space, children, alignItems = 'center', ...props }: HStackProps,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// biome-ignore lint/suspicious/noExplicitAny: none
|
||||
ref: ForwardedRef<any>,
|
||||
) {
|
||||
return (
|
||||
@@ -41,7 +41,7 @@ export type VStackProps = BaseStackProps & {
|
||||
|
||||
export const VStack = forwardRef(function VStack(
|
||||
{ className, space, children, ...props }: VStackProps,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// biome-ignore lint/suspicious/noExplicitAny: none
|
||||
ref: ForwardedRef<any>,
|
||||
) {
|
||||
return (
|
||||
@@ -65,7 +65,7 @@ type BaseStackProps = HTMLAttributes<HTMLElement> & {
|
||||
|
||||
const BaseStack = forwardRef(function BaseStack(
|
||||
{ className, alignItems, justifyContent, wrap, children, as, ...props }: BaseStackProps,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// biome-ignore lint/suspicious/noExplicitAny: none
|
||||
ref: ForwardedRef<any>,
|
||||
) {
|
||||
const Component = as ?? 'div';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
export function Table({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
@@ -57,7 +56,12 @@ export function TableHeaderCell({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<th className={classNames(className, 'py-2 [&:not(:first-child)]:pl-4 text-left text-text-subtle')}>
|
||||
<th
|
||||
className={classNames(
|
||||
className,
|
||||
'py-2 [&:not(:first-child)]:pl-4 text-left text-text-subtle',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
|
||||
@@ -50,7 +50,7 @@ export function Tabs({
|
||||
|
||||
// Update tabs when value changes
|
||||
useEffect(() => {
|
||||
const tabs = ref.current?.querySelectorAll<HTMLDivElement>(`[data-tab]`);
|
||||
const tabs = ref.current?.querySelectorAll<HTMLDivElement>('[data-tab]');
|
||||
for (const tab of tabs ?? []) {
|
||||
const v = tab.getAttribute('data-tab');
|
||||
const parent = tab.closest('.tabs-container');
|
||||
@@ -80,6 +80,7 @@ export function Tabs({
|
||||
)}
|
||||
>
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label={label}
|
||||
className={classNames(
|
||||
tabListClassName,
|
||||
@@ -162,13 +163,12 @@ export function Tabs({
|
||||
</Button>
|
||||
</RadioDropdown>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Button key={t.value} rightSlot={t.rightSlot} {...btnProps}>
|
||||
{t.label}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button key={t.value} rightSlot={t.rightSlot} {...btnProps}>
|
||||
{t.label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ShowToastRequest } from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import * as m from 'motion/react-m';
|
||||
import type { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { useKey } from 'react-use';
|
||||
import type { IconProps } from './Icon';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties, KeyboardEvent, ReactNode } from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { generateId } from '../../lib/generateId';
|
||||
import { Portal } from '../Portal';
|
||||
|
||||
@@ -91,6 +91,7 @@ export function Tooltip({ children, className, content, tabIndex, size = 'md' }:
|
||||
<Triangle className="text-border mb-2" />
|
||||
</div>
|
||||
</Portal>
|
||||
{/** biome-ignore lint/a11y/useSemanticElements: Needs to be usable in other buttons */}
|
||||
<span
|
||||
ref={triggerRef}
|
||||
role="button"
|
||||
@@ -124,6 +125,7 @@ function Triangle({ className }: { className?: string }) {
|
||||
'h-[0.5rem] w-[0.8rem]',
|
||||
)}
|
||||
>
|
||||
<title>Triangle</title>
|
||||
<polygon className="fill-surface-highlight" points="0,0 30,0 15,10" />
|
||||
<path
|
||||
d="M0 0 L15 9 L30 0"
|
||||
|
||||
@@ -9,7 +9,7 @@ interface Props {
|
||||
export function WebsocketStatusTag({ connection, className }: Props) {
|
||||
const { state, error } = connection;
|
||||
|
||||
let label;
|
||||
let label: string;
|
||||
let colorClass = 'text-text-subtle';
|
||||
|
||||
if (error) {
|
||||
|
||||
@@ -11,9 +11,18 @@ import {
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
import classNames from 'classnames';
|
||||
import type { ComponentType, MouseEvent, ReactElement, Ref, RefAttributes } from 'react';
|
||||
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useKey, useKeyPressEvent } from 'react-use';
|
||||
import type { HotkeyAction, HotKeyOptions } from '../../../hooks/useHotKey';
|
||||
import type { HotKeyOptions, HotkeyAction } from '../../../hooks/useHotKey';
|
||||
import { useHotKey } from '../../../hooks/useHotKey';
|
||||
import { computeSideForDragMove } from '../../../lib/dnd';
|
||||
import { jotaiStore } from '../../../lib/jotai';
|
||||
@@ -102,6 +111,7 @@ function TreeInner<T extends { id: string }>(
|
||||
}, []);
|
||||
|
||||
// Select the first item on first render
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: Only used for initial render
|
||||
useEffect(() => {
|
||||
const ids = jotaiStore.get(selectedIdsFamily(treeId));
|
||||
const fallback = selectableItems[0];
|
||||
@@ -112,7 +122,6 @@ function TreeInner<T extends { id: string }>(
|
||||
lastId: fallback.node.item.id,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [treeId]);
|
||||
|
||||
const handleCloseContextMenu = useCallback(() => {
|
||||
@@ -129,11 +138,10 @@ function TreeInner<T extends { id: string }>(
|
||||
);
|
||||
if ($el == null) {
|
||||
return false;
|
||||
} else {
|
||||
$el.focus();
|
||||
$el.scrollIntoView({ block: 'nearest' });
|
||||
return true;
|
||||
}
|
||||
$el.focus();
|
||||
$el.scrollIntoView({ block: 'nearest' });
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const ensureTabbableItem = useCallback(() => {
|
||||
@@ -166,7 +174,7 @@ function TreeInner<T extends { id: string }>(
|
||||
useEffect(() => {
|
||||
const unsub = jotaiStore.sub(collapsedFamily(treeId), ensureTabbableItem);
|
||||
return unsub;
|
||||
}, [ensureTabbableItem, isTreeFocused, selectableItems, treeId, tryFocus]);
|
||||
}, [ensureTabbableItem, treeId]);
|
||||
|
||||
// Ensure there's always a tabbable item after render
|
||||
useEffect(() => {
|
||||
@@ -224,13 +232,12 @@ function TreeInner<T extends { id: string }>(
|
||||
if (isSelected) {
|
||||
// If right-clicked an item that was in the multiple-selection, use the entire selection
|
||||
return getContextMenu(items);
|
||||
} else {
|
||||
// If right-clicked an item that was NOT in the multiple-selection, just use that one
|
||||
// Also update the selection with it
|
||||
setSelected([item.id], false);
|
||||
jotaiStore.set(focusIdsFamily(treeId), (prev) => ({ ...prev, lastId: item.id }));
|
||||
return getContextMenu([item]);
|
||||
}
|
||||
// If right-clicked an item that was NOT in the multiple-selection, just use that one
|
||||
// Also update the selection with it
|
||||
setSelected([item.id], false);
|
||||
jotaiStore.set(focusIdsFamily(treeId), (prev) => ({ ...prev, lastId: item.id }));
|
||||
return getContextMenu([item]);
|
||||
};
|
||||
}, [getContextMenu, selectableItems, setSelected, treeId]);
|
||||
|
||||
@@ -728,6 +735,7 @@ function DropRegionAfterList({
|
||||
onContextMenu?: (e: MouseEvent<HTMLDivElement>) => void;
|
||||
}) {
|
||||
const { setNodeRef } = useDroppable({ id });
|
||||
// biome-ignore lint/a11y/noStaticElementInteractions: Meh
|
||||
return <div ref={setNodeRef} onContextMenu={onContextMenu} />;
|
||||
}
|
||||
|
||||
@@ -758,7 +766,7 @@ function TreeHotKey<T extends { id: string }>({
|
||||
enable: () => {
|
||||
if (enable == null) return true;
|
||||
if (typeof enable === 'function') return enable();
|
||||
else return enable;
|
||||
return enable;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ export function TreeDragOverlay<T extends { id: string }>({
|
||||
return (
|
||||
<DragOverlay dropAnimation={null}>
|
||||
<TreeItemList
|
||||
treeId={treeId + '.dragging'}
|
||||
treeId={`${treeId}.dragging`}
|
||||
nodes={selectableItems.filter((i) => draggingItems.includes(i.node.item.id))}
|
||||
getItemKey={getItemKey}
|
||||
ItemInner={ItemInner}
|
||||
|
||||
@@ -19,6 +19,7 @@ export const TreeIndentGuide = memo(function TreeIndentGuide({
|
||||
<div className="flex">
|
||||
{Array.from({ length: depth }).map((_, i) => (
|
||||
<div
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: none
|
||||
key={i}
|
||||
className={classNames(
|
||||
'w-[calc(1rem+0.5px)] border-r border-r-text-subtlest',
|
||||
|
||||
@@ -3,14 +3,15 @@ import { useDndContext, useDndMonitor, useDraggable, useDroppable } from '@dnd-k
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { selectAtom } from 'jotai/utils';
|
||||
import type React from 'react';
|
||||
import type { MouseEvent, PointerEvent } from 'react';
|
||||
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { computeSideForDragMove } from '../../../lib/dnd';
|
||||
import { jotaiStore } from '../../../lib/jotai';
|
||||
import type { ContextMenuProps, DropdownItem } from '../Dropdown';
|
||||
import { ContextMenu } from '../Dropdown';
|
||||
import { Icon } from '../Icon';
|
||||
import { collapsedFamily, isCollapsedFamily, isLastFocusedFamily, isSelectedFamily, } from './atoms';
|
||||
import { collapsedFamily, isCollapsedFamily, isLastFocusedFamily, isSelectedFamily } from './atoms';
|
||||
import type { TreeNode } from './common';
|
||||
import { getNodeKey } from './common';
|
||||
import type { TreeProps } from './Tree';
|
||||
@@ -84,7 +85,7 @@ function TreeItem_<T extends { id: string }>({
|
||||
},
|
||||
scrollIntoView: () => {
|
||||
listItemRef.current?.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
},
|
||||
}),
|
||||
[editing, getEditOptions],
|
||||
);
|
||||
@@ -272,10 +273,6 @@ function TreeItem_<T extends { id: string }>({
|
||||
return (
|
||||
<li
|
||||
ref={listItemRef}
|
||||
role="treeitem"
|
||||
aria-level={depth + 1}
|
||||
aria-expanded={node.children == null ? undefined : !isCollapsed}
|
||||
aria-selected={isSelected}
|
||||
onContextMenu={handleContextMenu}
|
||||
className={classNames(
|
||||
className,
|
||||
@@ -304,6 +301,7 @@ function TreeItem_<T extends { id: string }>({
|
||||
)}
|
||||
{node.children != null ? (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
className="h-full pl-[0.5rem] outline-none"
|
||||
onClick={toggleCollapsed}
|
||||
|
||||
@@ -29,7 +29,7 @@ export function TreeItemList<T extends { id: string }>({
|
||||
...props
|
||||
}: TreeItemListProps<T>) {
|
||||
return (
|
||||
<ul role="tree" style={style} className={className}>
|
||||
<ul style={style} className={className}>
|
||||
<TreeDropMarker node={null} treeId={treeId} index={0} />
|
||||
{nodes.map((child, i) => (
|
||||
<Fragment key={getItemKey(child.node.item)}>
|
||||
|
||||
@@ -21,7 +21,7 @@ export const focusIdsFamily = atomFamily((_treeId: string) => {
|
||||
|
||||
export const isLastFocusedFamily = atomFamily(
|
||||
({ treeId, itemId }: { treeId: string; itemId: string }) =>
|
||||
selectAtom(focusIdsFamily(treeId), (v) => v.lastId == itemId, Object.is),
|
||||
selectAtom(focusIdsFamily(treeId), (v) => v.lastId === itemId, Object.is),
|
||||
(a, b) => a.treeId === b.treeId && a.itemId === b.itemId,
|
||||
);
|
||||
|
||||
|
||||
@@ -35,8 +35,13 @@ export function equalSubtree<T extends { id: string }>(
|
||||
if (getNodeKey(a, getItemKey) !== getNodeKey(b, getItemKey)) return false;
|
||||
const ak = a.children ?? [];
|
||||
const bk = b.children ?? [];
|
||||
if (ak.length !== bk.length) return false;
|
||||
|
||||
if (ak.length !== bk.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < ak.length; i++) {
|
||||
// biome-ignore lint/style/noNonNullAssertion: none
|
||||
if (!equalSubtree(ak[i]!, bk[i]!, getItemKey)) return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ export function useSelectableItems<T extends { id: string }>(root: TreeNode<T>)
|
||||
const selectableItems: SelectableTreeNode<T>[] = [];
|
||||
|
||||
// Put requests and folders into a tree structure
|
||||
const next = (node: TreeNode<T>, depth: number = 0) => {
|
||||
const next = (node: TreeNode<T>, depth = 0) => {
|
||||
if (node.children == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user