Pair checkboxes and fix twig indent

This commit is contained in:
Gregory Schier
2023-03-20 00:03:33 -07:00
parent ae65f222bc
commit 90294fbb5d
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/generator": "^1.2.2",
"@lezer/highlight": "^1.1.3", "@lezer/highlight": "^1.1.3",
"@lezer/lr": "^1.3.3", "@lezer/lr": "^1.3.3",
"@radix-ui/react-checkbox": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.2", "@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.2", "@radix-ui/react-dropdown-menu": "^2.0.2",
"@radix-ui/react-icons": "^1.2.0", "@radix-ui/react-icons": "^1.2.0",
@@ -1276,6 +1277,39 @@
"react-dom": "^16.8 || ^17.0 || ^18.0" "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": { "node_modules/@radix-ui/react-collection": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.1.tgz", "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" "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": { "node_modules/@radix-ui/react-use-rect": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.0.tgz", "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-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": { "@radix-ui/react-collection": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.1.tgz",
@@ -9287,6 +9359,14 @@
"@babel/runtime": "^7.13.10" "@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": { "@radix-ui/react-use-rect": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.0.tgz", "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/generator": "^1.2.2",
"@lezer/highlight": "^1.1.3", "@lezer/highlight": "^1.1.3",
"@lezer/lr": "^1.3.3", "@lezer/lr": "^1.3.3",
"@radix-ui/react-checkbox": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.2", "@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.2", "@radix-ui/react-dropdown-menu": "^2.0.2",
"@radix-ui/react-icons": "^1.2.0", "@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)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct HttpRequestHeader { pub struct HttpRequestHeader {
#[serde(default)]
pub enabled: bool,
pub name: String, pub name: String,
pub value: String, pub value: String,
} }

View File

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

View File

@@ -309,7 +309,7 @@ const _SidebarItem = forwardRef(function SidebarItem(
className={classnames( className={classnames(
buttonClassName, buttonClassName,
'w-full', 'w-full',
editing && 'focus-within:border-blue-400/40', editing && 'focus-within:border-focus',
active active
? 'bg-gray-200/70 text-gray-900' ? 'bg-gray-200/70 text-gray-900'
: 'text-gray-600 group-hover/item:text-gray-800 active:bg-gray-200/30', : '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 classnames from 'classnames';
import { useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useCreateWorkspace } from '../hooks/useCreateWorkspace'; import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
@@ -24,7 +24,10 @@ export function WorkspaceDropdown({ className }: Props) {
label: w.name, label: w.name,
value: w.id, value: w.id,
leftSlot: activeWorkspace?.id === w.id ? <Icon icon="check" /> : <Icon icon="empty" />, 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 [ 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 <D.Item
asChild asChild
disabled={disabled} disabled={disabled}
className={classnames(className, disabled && 'opacity-30')} className={classnames(className, disabled && 'opacity-disabled')}
{...props} {...props}
> >
<ItemInner leftSlot={leftSlot} rightSlot={rightSlot}> <ItemInner leftSlot={leftSlot} rightSlot={rightSlot}>

View File

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

View File

@@ -1,8 +1,10 @@
import { LanguageSupport, LRLanguage } from '@codemirror/language'; import { LanguageSupport, LRLanguage } from '@codemirror/language';
import { parser } from './text'; import { parser } from './text';
export const textLanguageName = 'text';
const textLanguage = LRLanguage.define({ const textLanguage = LRLanguage.define({
name: 'text', name: textLanguageName,
parser, parser,
languageData: {}, 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 { parseMixed } from '@lezer/common';
import type { GenericCompletionConfig } from '../genericCompletion'; import type { GenericCompletionConfig } from '../genericCompletion';
import { genericCompletion } from '../genericCompletion'; import { genericCompletion } from '../genericCompletion';
import { textLanguageName } from '../text/extension';
import { placeholders } from '../widgets'; import { placeholders } from '../widgets';
import { completions } from './completion'; import { completions } from './completion';
import { parser as twigParser } from './twig'; import { parser as twigParser } from './twig';
export function twig(base?: LanguageSupport, autocomplete?: GenericCompletionConfig) { export function twig(base: LanguageSupport, autocomplete?: GenericCompletionConfig) {
const language = mixedOrPlainLanguage(base); const language = mixLanguage(base);
const additionalCompletion = const additionalCompletion = autocomplete
autocomplete && base ? [language.data.of({ autocomplete: genericCompletion(autocomplete) })]
? [language.data.of({ autocomplete: genericCompletion(autocomplete) })] : [];
: [];
const completion = language.data.of({ const completion = language.data.of({
autocomplete: completions, autocomplete: completions,
}); });
const languageSupport = new LanguageSupport(language, [completion, ...additionalCompletion]);
if (base) { if (base) {
const completion2 = base.language.data.of({ autocomplete: completions }); const completionBase = base.language.data.of({
const languageSupport2 = new LanguageSupport(base.language, [completion2]); autocomplete: completions,
return [languageSupport, languageSupport2, base.support]; });
return [
language,
completion,
completionBase,
base.support,
// placeholders,
...additionalCompletion,
];
} else { } else {
return [languageSupport]; return [language, completion, placeholders];
} }
} }
function mixedOrPlainLanguage(base?: LanguageSupport): LRLanguage { function mixLanguage(base: LanguageSupport): LRLanguage {
const name = 'twig'; const name = 'twig';
if (!base) {
return LRLanguage.define({ name, parser: twigParser });
}
const parser = twigParser.configure({ const parser = twigParser.configure({
wrap: parseMixed((node) => { 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 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 null;
} }
return { return {
parser: base.language.parser, 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'; import { styleTags, tags as t } from '@lezer/highlight';
export const highlight = styleTags({ export const highlight = styleTags({
Open: t.meta, Open: t.tagName,
Close: t.meta, Close: t.tagName,
Content: t.comment, Content: t.keyword,
Template: t.comment,
}); });

View File

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

View File

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

View File

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

View File

@@ -63,7 +63,7 @@ export function Input({
className={classnames( className={classnames(
containerClassName, containerClassName,
'relative w-full rounded-md text-gray-900', '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 === 'md' && 'h-9',
size === 'sm' && 'h-7', size === 'sm' && 'h-7',
)} )}

View File

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

View File

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

View File

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

View File

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

View File

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