Pair checkboxes and fix twig indent

This commit is contained in:
Gregory Schier
2023-03-20 00:03:33 -07:00
parent 31e09aba4b
commit d57dfbf225
23 changed files with 286 additions and 115 deletions

80
package-lock.json generated
View File

@@ -18,6 +18,7 @@
"@lezer/generator": "^1.2.2",
"@lezer/highlight": "^1.1.3",
"@lezer/lr": "^1.3.3",
"@radix-ui/react-checkbox": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.2",
"@radix-ui/react-icons": "^1.2.0",
@@ -1276,6 +1277,39 @@
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-checkbox": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.0.3.tgz",
"integrity": "sha512-55B8/vKzTuzxllH5sGJO4zaBf9gYpJuJRRzaOKm+0oAefRnMvbf+Kgww7IOANVN0w3z7agFJgtnXaZl8Uj95AA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-presence": "1.0.0",
"@radix-ui/react-primitive": "1.0.2",
"@radix-ui/react-use-controllable-state": "1.0.0",
"@radix-ui/react-use-previous": "1.0.0",
"@radix-ui/react-use-size": "1.0.0"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz",
"integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "1.0.1"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-collection": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.1.tgz",
@@ -1735,6 +1769,17 @@
"react": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-use-previous": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.0.tgz",
"integrity": "sha512-RG2K8z/K7InnOKpq6YLDmT49HGjNmrK+fr82UCVKT2sW0GYfVnYp4wZWBooT/EYfQ5faA9uIjvsuMMhH61rheg==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-use-rect": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.0.tgz",
@@ -8930,6 +8975,33 @@
"@radix-ui/react-primitive": "1.0.1"
}
},
"@radix-ui/react-checkbox": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.0.3.tgz",
"integrity": "sha512-55B8/vKzTuzxllH5sGJO4zaBf9gYpJuJRRzaOKm+0oAefRnMvbf+Kgww7IOANVN0w3z7agFJgtnXaZl8Uj95AA==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-presence": "1.0.0",
"@radix-ui/react-primitive": "1.0.2",
"@radix-ui/react-use-controllable-state": "1.0.0",
"@radix-ui/react-use-previous": "1.0.0",
"@radix-ui/react-use-size": "1.0.0"
},
"dependencies": {
"@radix-ui/react-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz",
"integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "1.0.1"
}
}
}
},
"@radix-ui/react-collection": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.1.tgz",
@@ -9287,6 +9359,14 @@
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/react-use-previous": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.0.tgz",
"integrity": "sha512-RG2K8z/K7InnOKpq6YLDmT49HGjNmrK+fr82UCVKT2sW0GYfVnYp4wZWBooT/EYfQ5faA9uIjvsuMMhH61rheg==",
"requires": {
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/react-use-rect": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.0.tgz",

View File

@@ -25,6 +25,7 @@
"@lezer/generator": "^1.2.2",
"@lezer/highlight": "^1.1.3",
"@lezer/lr": "^1.3.3",
"@radix-ui/react-checkbox": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.2",
"@radix-ui/react-icons": "^1.2.0",

Binary file not shown.

View File

@@ -18,6 +18,8 @@ pub struct Workspace {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HttpRequestHeader {
#[serde(default)]
pub enabled: bool,
pub name: String,
pub value: String,
}

View File

@@ -1,13 +1,11 @@
import classnames from 'classnames';
import { useCallback, useMemo } from 'react';
import { act } from 'react-dom/test-utils';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useKeyValue } from '../hooks/useKeyValue';
import { useUpdateRequest } from '../hooks/useUpdateRequest';
import { tryFormatJson } from '../lib/formatters';
import type { HttpHeader } from '../lib/models';
import { Editor } from './core/Editor';
import { PairEditor } from './core/PairEditor';
import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs';
import { GraphQLEditor } from './GraphQLEditor';
@@ -26,7 +24,7 @@ export function RequestPane({ fullHeight, className }: Props) {
const updateRequest = useUpdateRequest(activeRequestId);
const activeTab = useKeyValue<string>({
key: ['active_request_body_tab', activeRequestId ?? 'n/a'],
initialValue: 'body',
defaultValue: 'body',
});
const tabs: TabItem[] = useMemo(
@@ -40,6 +38,7 @@ export function RequestPane({ fullHeight, className }: Props) {
items: [
{ label: 'No Body', value: 'nobody' },
{ label: 'JSON', value: 'json' },
{ label: 'XML', value: 'xml' },
{ label: 'GraphQL', value: 'graphql' },
],
},
@@ -57,53 +56,65 @@ export function RequestPane({ fullHeight, className }: Props) {
[],
);
if (activeRequest === null) return null;
return (
<div className={classnames(className, 'py-3 grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1')}>
<UrlBar className="pl-3" request={activeRequest} />
<Tabs
value={activeTab.value}
onChangeValue={activeTab.set}
tabs={tabs}
className="mt-2"
tabListClassName="pl-3"
label="Request body"
>
<TabContent value="headers">
<HeaderEditor
key={activeRequestId}
headers={activeRequest.headers}
onChange={handleHeadersChange}
/>
</TabContent>
<TabContent value="params">
<ParametersEditor key={activeRequestId} parameters={[]} onChange={() => null} />
</TabContent>
<TabContent value="body" className="pl-3 mt-1">
{activeRequest.bodyType === 'json' ? (
<Editor
key={activeRequest.id}
useTemplating
className="!bg-gray-50"
heightMode={fullHeight ? 'full' : 'auto'}
defaultValue={activeRequest.body ?? ''}
contentType="application/json"
onChange={handleBodyChange}
format={activeRequest.bodyType === 'json' ? (v) => tryFormatJson(v) : undefined}
/>
) : activeRequest.bodyType === 'graphql' ? (
<GraphQLEditor
key={activeRequest.id}
className="!bg-gray-50"
defaultValue={activeRequest?.body ?? ''}
onChange={handleBodyChange}
/>
) : (
<div className="h-full text-gray-400 flex items-center justify-center">No Body</div>
)}
</TabContent>
</Tabs>
{activeRequest && (
<>
<UrlBar className="pl-3" request={activeRequest} />
<Tabs
value={activeTab.value}
onChangeValue={activeTab.set}
tabs={tabs}
className="mt-2"
tabListClassName="pl-3"
label="Request body"
>
<TabContent value="headers">
<HeaderEditor
key={activeRequestId}
headers={activeRequest.headers}
onChange={handleHeadersChange}
/>
</TabContent>
<TabContent value="params">
<ParametersEditor key={activeRequestId} parameters={[]} onChange={() => null} />
</TabContent>
<TabContent value="body" className="pl-3 mt-1">
{activeRequest.bodyType === 'json' ? (
<Editor
key={activeRequest.id}
useTemplating
className="!bg-gray-50"
heightMode={fullHeight ? 'full' : 'auto'}
defaultValue={activeRequest.body ?? ''}
contentType="application/json"
onChange={handleBodyChange}
format={(v) => tryFormatJson(v)}
/>
) : activeRequest.bodyType === 'xml' ? (
<Editor
key={activeRequest.id}
useTemplating
className="!bg-gray-50"
heightMode={fullHeight ? 'full' : 'auto'}
defaultValue={activeRequest.body ?? ''}
contentType="text/xml"
onChange={handleBodyChange}
/>
) : activeRequest.bodyType === 'graphql' ? (
<GraphQLEditor
key={activeRequest.id}
className="!bg-gray-50"
defaultValue={activeRequest?.body ?? ''}
onChange={handleBodyChange}
/>
) : (
<div className="h-full text-gray-400 flex items-center justify-center">No Body</div>
)}
</TabContent>
</Tabs>
</>
)}
</div>
);
}

View File

@@ -309,7 +309,7 @@ const _SidebarItem = forwardRef(function SidebarItem(
className={classnames(
buttonClassName,
'w-full',
editing && 'focus-within:border-blue-400/40',
editing && 'focus-within:border-focus',
active
? 'bg-gray-200/70 text-gray-900'
: 'text-gray-600 group-hover/item:text-gray-800 active:bg-gray-200/30',

View File

@@ -1,5 +1,5 @@
import classnames from 'classnames';
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
@@ -24,7 +24,10 @@ export function WorkspaceDropdown({ className }: Props) {
label: w.name,
value: w.id,
leftSlot: activeWorkspace?.id === w.id ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: () => navigate(`/workspaces/${w.id}`),
onSelect: () => {
if (w.id === activeWorkspace?.id) return;
navigate(`/workspaces/${w.id}`);
},
}));
return [

View File

@@ -0,0 +1,35 @@
import type { CheckedState } from '@radix-ui/react-checkbox';
import * as CB from '@radix-ui/react-checkbox';
import classnames from 'classnames';
import { Icon } from './Icon';
interface Props {
checked: CheckedState;
onChange: (checked: CheckedState) => void;
disabled?: boolean;
className?: string;
}
export function Checkbox({ checked, onChange, className, disabled }: Props) {
return (
<CB.Root
disabled={disabled}
checked={checked}
onCheckedChange={onChange}
className={classnames(
className,
'w-5 h-5 border border-gray-200 rounded',
'focus:border-focus',
'disabled:opacity-disabled',
'outline-none',
checked && 'bg-gray-200/10',
// Remove focus style
)}
>
<CB.Indicator className="flex items-center justify-center">
{checked === 'indeterminate' && <Icon icon="dividerH" />}
{checked === true && <Icon icon="check" />}
</CB.Indicator>
</CB.Root>
);
}

View File

@@ -182,7 +182,7 @@ const DropdownMenuItem = memo(function DropdownMenuItem({
<D.Item
asChild
disabled={disabled}
className={classnames(className, disabled && 'opacity-30')}
className={classnames(className, disabled && 'opacity-disabled')}
{...props}
>
<ItemInner leftSlot={leftSlot} rightSlot={rightSlot}>

View File

@@ -33,7 +33,6 @@ import {
} from '@codemirror/view';
import { tags as t } from '@lezer/highlight';
import { graphqlLanguageSupport } from 'cm6-graphql';
import type { GenericCompletionOption } from './genericCompletion';
import type { EditorProps } from './index';
import { text } from './text/extension';
import { twig } from './twig/extension';

View File

@@ -1,8 +1,10 @@
import { LanguageSupport, LRLanguage } from '@codemirror/language';
import { parser } from './text';
export const textLanguageName = 'text';
const textLanguage = LRLanguage.define({
name: 'text',
name: textLanguageName,
parser,
languageData: {},
});

View File

@@ -1,47 +1,53 @@
import { LanguageSupport, LRLanguage } from '@codemirror/language';
import type { LanguageSupport } from '@codemirror/language';
import { LRLanguage } from '@codemirror/language';
import { parseMixed } from '@lezer/common';
import type { GenericCompletionConfig } from '../genericCompletion';
import { genericCompletion } from '../genericCompletion';
import { textLanguageName } from '../text/extension';
import { placeholders } from '../widgets';
import { completions } from './completion';
import { parser as twigParser } from './twig';
export function twig(base?: LanguageSupport, autocomplete?: GenericCompletionConfig) {
const language = mixedOrPlainLanguage(base);
const additionalCompletion =
autocomplete && base
? [language.data.of({ autocomplete: genericCompletion(autocomplete) })]
: [];
export function twig(base: LanguageSupport, autocomplete?: GenericCompletionConfig) {
const language = mixLanguage(base);
const additionalCompletion = autocomplete
? [language.data.of({ autocomplete: genericCompletion(autocomplete) })]
: [];
const completion = language.data.of({
autocomplete: completions,
});
const languageSupport = new LanguageSupport(language, [completion, ...additionalCompletion]);
if (base) {
const completion2 = base.language.data.of({ autocomplete: completions });
const languageSupport2 = new LanguageSupport(base.language, [completion2]);
return [languageSupport, languageSupport2, base.support];
const completionBase = base.language.data.of({
autocomplete: completions,
});
return [
language,
completion,
completionBase,
base.support,
// placeholders,
...additionalCompletion,
];
} else {
return [languageSupport];
return [language, completion, placeholders];
}
}
function mixedOrPlainLanguage(base?: LanguageSupport): LRLanguage {
function mixLanguage(base: LanguageSupport): LRLanguage {
const name = 'twig';
if (!base) {
return LRLanguage.define({ name, parser: twigParser });
}
const parser = twigParser.configure({
wrap: parseMixed((node) => {
console.log('HELLO', node.type.name, node.type.isTop);
// If the base language is text, we can overwrite at the top
if (base.language.name !== 'text' && !node.type.isTop) {
if (base.language.name !== textLanguageName && !node.type.isTop) {
return null;
}
return {
parser: base.language.parser,
overlay: (node) => node.type.name === 'Text' || node.type.name === 'Template',
overlay: (node) => node.type.name === 'Text',
};
}),
});

View File

@@ -1,8 +1,7 @@
import { styleTags, tags as t } from '@lezer/highlight';
export const highlight = styleTags({
Open: t.meta,
Close: t.meta,
Content: t.comment,
Template: t.comment,
Open: t.tagName,
Close: t.tagName,
Content: t.keyword,
});

View File

@@ -1,17 +1,17 @@
@top Template { Tag | Text }
@top Template { (Tag | Text)* }
@local tokens {
Close { "]}" }
@else Content
}
@skip {} {
@skip { } {
Open { "${[" }
Tag { Open (Content)+ Close }
}
@tokens {
Text { _ }
Text { ![$] Text? }
}
@external propSource highlight from "./highlight"

View File

@@ -3,15 +3,15 @@ import {LRParser, LocalTokenGroup} from "@lezer/lr"
import {highlight} from "./highlight"
export const parser = LRParser.deserialize({
version: 14,
states: "!QOQOPOOOOOO'#C_'#C_OYOQO'#C^QOOOOOOOOO'#Cc'#CcO_OQO,58xOOOO-E6a-E6aOOOO1G.d1G.d",
stateData: "g~OUROXPO~OSSO~OSSOTVO~O",
goto: "eWPPX[PPP_RRORQOQTQRUT",
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 Open Content Close Text",
maxTerm: 9,
maxTerm: 10,
propSources: [highlight],
skippedNodes: [0],
repeatNodeCount: 1,
tokenData: "!S~RTOtbtugu;'Sb;'S;=`z;=`Ob~gOU~~lPU~#o#po~rP!}#Ou~zOX~~!PPU~;=`<%lb",
repeatNodeCount: 2,
tokenData: "![~RTOtbtuyu;'Sb;'S;=`s<%lOb~gSU~Otbu;'Sb;'S;=`s<%lOb~vP;=`<%lb~|P#o#p!P~!SP!}#O!V~![OY~",
tokenizers: [1, new LocalTokenGroup("b~RP#P#QU~XP#q#r[~aOT~~", 17, 4)],
topRules: {"Template":[0,1]},
tokenPrec: 0

View File

@@ -1,11 +1,13 @@
import {
ArchiveIcon,
CameraIcon,
CheckboxIcon,
CheckIcon,
ClockIcon,
CodeIcon,
ColorWheelIcon,
Cross2Icon,
DividerHorizontalIcon,
DotsHorizontalIcon,
DotsVerticalIcon,
DragHandleDots2Icon,
@@ -35,6 +37,7 @@ const icons = {
archive: ArchiveIcon,
camera: CameraIcon,
check: CheckIcon,
checkbox: CheckboxIcon,
clock: ClockIcon,
code: CodeIcon,
colorWheel: ColorWheelIcon,
@@ -50,6 +53,7 @@ const icons = {
moon: MoonIcon,
paperPlane: PaperPlaneIcon,
plus: PlusIcon,
dividerH: DividerHorizontalIcon,
plusCircle: PlusCircledIcon,
question: QuestionMarkIcon,
rows: RowsIcon,

View File

@@ -63,7 +63,7 @@ export function Input({
className={classnames(
containerClassName,
'relative w-full rounded-md text-gray-900',
'border border-gray-200 focus-within:border-blue-400/40',
'border border-gray-200 focus-within:border-focus',
size === 'md' && 'h-9',
size === 'sm' && 'h-7',
)}

View File

@@ -1,9 +1,11 @@
import type { CheckedState } from '@radix-ui/react-checkbox';
import classnames from 'classnames';
import React, { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd';
import { v4 as uuid } from 'uuid';
import { DropMarker } from '../DropMarker';
import { Checkbox } from './Checkbox';
import type { GenericCompletionConfig } from './Editor/genericCompletion';
import { Icon } from './Icon';
import { IconButton } from './IconButton';
@@ -20,6 +22,7 @@ export type PairEditorProps = {
};
type Pair = {
enabled?: boolean;
name: string;
value: string;
};
@@ -84,20 +87,17 @@ export const PairEditor = memo(function PairEditor({
[hoveredIndex],
);
const handleChangeHeader = useCallback((pair: PairContainer) => {
setPairsAndSave((pairs) => pairs.map((p) => (pair.id !== p.id ? p : pair)));
}, []);
const handleChange = useCallback(
(pair: PairContainer) =>
setPairsAndSave((pairs) => pairs.map((p) => (pair.id !== p.id ? p : pair))),
[],
);
// Ensure there's always at least one pair
useEffect(() => {
if (pairs.length === 0) {
setPairs((pairs) => [...pairs, newPairContainer()]);
}
}, [pairs]);
const handleDelete = useCallback((pair: PairContainer) => {
setPairsAndSave((oldPairs) => oldPairs.filter((p) => p.id !== pair.id));
}, []);
const handleDelete = useCallback(
(pair: PairContainer) =>
setPairsAndSave((oldPairs) => oldPairs.filter((p) => p.id !== pair.id)),
[],
);
const handleFocus = useCallback(
(pair: PairContainer) => {
@@ -109,6 +109,13 @@ export const PairEditor = memo(function PairEditor({
[pairs],
);
// Ensure there's always at least one pair
useEffect(() => {
if (pairs.length === 0) {
setPairs((pairs) => [...pairs, newPairContainer()]);
}
}, [pairs]);
return (
<div
className={classnames(
@@ -126,11 +133,11 @@ export const PairEditor = memo(function PairEditor({
<FormRow
pairContainer={p}
isLast={isLast}
onChange={handleChangeHeader}
nameAutocomplete={nameAutocomplete}
valueAutocomplete={valueAutocomplete}
namePlaceholder={namePlaceholder}
valuePlaceholder={valuePlaceholder}
onChange={handleChange}
onFocus={handleFocus}
onDelete={isLast ? undefined : handleDelete}
onEnd={handleEnd}
@@ -177,14 +184,20 @@ const FormRow = memo(function FormRow({
const { id } = pairContainer;
const ref = useRef<HTMLDivElement>(null);
const handleChangeEnabled = useMemo(
() => (enabled: CheckedState) =>
onChange({ id, pair: { ...pairContainer.pair, enabled: !!enabled } }),
[onChange, pairContainer.pair.name, pairContainer.pair.value],
);
const handleChangeName = useMemo(
() => (name: string) => onChange({ id, pair: { name, value: pairContainer.pair.value } }),
[onChange, pairContainer.pair.value],
() => (name: string) => onChange({ id, pair: { ...pairContainer.pair, name } }),
[onChange, pairContainer.pair.value, pairContainer.pair.enabled],
);
const handleChangeValue = useMemo(
() => (value: string) => onChange({ id, pair: { value, name: pairContainer.pair.name } }),
[onChange, pairContainer.pair.name],
() => (value: string) => onChange({ id, pair: { ...pairContainer.pair, value } }),
[onChange, pairContainer.pair.name, pairContainer.pair.enabled],
);
const nameEditorConfig = useMemo(
@@ -231,7 +244,11 @@ const FormRow = memo(function FormRow({
return (
<div
ref={ref}
className="pb-2 group grid grid-cols-[auto_minmax(0,1fr)_minmax(0,1fr)_auto] grid-rows-1 gap-2 items-center"
className={classnames(
'pb-2 group grid grid-cols-[auto_auto_minmax(0,1fr)_minmax(0,1fr)_auto]',
'grid-rows-1 gap-2 items-center',
!pairContainer.pair.enabled && 'opacity-60',
)}
>
{!isLast ? (
<div
@@ -245,6 +262,12 @@ const FormRow = memo(function FormRow({
) : (
<span className="w-1" />
)}
<Checkbox
disabled={isLast}
checked={!!pairContainer.pair.enabled}
onChange={handleChangeEnabled}
className={isLast ? '!opacity-disabled' : undefined}
/>
<Input
hideLabel
containerClassName={classnames(isLast && 'border-dashed')}
@@ -283,5 +306,5 @@ const FormRow = memo(function FormRow({
});
const newPairContainer = (pair?: Pair): PairContainer => {
return { pair: pair ?? { name: '', value: '' }, id: uuid() };
return { pair: pair ?? { name: '', value: '', enabled: true }, id: uuid() };
};

View File

@@ -22,8 +22,8 @@ export type TabItem = {
interface Props {
label: string;
value?: string;
onChangeValue: (value: string) => void;
value: string;
tabs: TabItem[];
tabListClassName?: string;
className?: string;

View File

@@ -16,16 +16,15 @@ export function keyValueQueryKey({
export function useKeyValue<T extends string | number | boolean>({
namespace = DEFAULT_NAMESPACE,
key,
initialValue,
defaultValue,
}: {
namespace?: string;
key: string | string[];
initialValue: T;
defaultValue: T;
}) {
const query = useQuery<T>({
initialData: initialValue,
queryKey: keyValueQueryKey({ namespace, key }),
queryFn: async () => getKeyValue({ namespace, key, fallback: initialValue }),
queryFn: async () => getKeyValue({ namespace, key, fallback: defaultValue }),
});
const mutate = useMutation<T, unknown, T>({
@@ -34,6 +33,7 @@ export function useKeyValue<T extends string | number | boolean>({
return {
value: query.data,
isLoading: query.isLoading,
set: (value: T) => mutate.mutate(value),
};
}

View File

@@ -1,10 +1,10 @@
import { useKeyValue } from './useKeyValue';
export function useResponseViewMode(requestId?: string): [string, () => void] {
export function useResponseViewMode(requestId?: string): [string | undefined, () => void] {
const v = useKeyValue<string>({
namespace: 'app',
key: ['response_view_mode', requestId ?? 'n/a'],
initialValue: 'pretty',
defaultValue: 'pretty',
});
const toggle = () => {

View File

@@ -14,6 +14,7 @@ export interface Workspace extends BaseModel {
export interface HttpHeader {
name: string;
value: string;
enabled?: boolean;
}
export interface HttpRequest extends BaseModel {

View File

@@ -6,7 +6,11 @@ module.exports = {
"./src-web/**/*.{html,js,jsx,ts,tsx}"
],
theme: {
extend: {},
extend: {
opacity: {
'disabled': '0.3',
}
},
fontFamily: {
"mono": ["JetBrains Mono", "Menlo", "monospace"],
"sans": ["Inter", "sans-serif"],
@@ -21,6 +25,7 @@ module.exports = {
"5xl": "3.052rem"
},
colors: {
focus: "hsl(var(--color-blue-500) / 0.6)",
highlight: "hsl(var(--color-gray-200) / 0.3)",
transparent: "transparent",
white: "hsl(0 100% 100% / <alpha-value>)",