mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-23 18:01:08 +01:00
OAuth 2 (#158)
This commit is contained in:
@@ -153,9 +153,10 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
||||
label: 'Send Request',
|
||||
onSelect: () => sendRequest(activeRequest.id),
|
||||
});
|
||||
for (const a of httpRequestActions) {
|
||||
for (let i = 0; i < httpRequestActions.length; i++) {
|
||||
const a = httpRequestActions[i]!;
|
||||
commands.push({
|
||||
key: a.key,
|
||||
key: `http_request_action.${i}`,
|
||||
label: a.label,
|
||||
onSelect: () => a.call(activeRequest),
|
||||
});
|
||||
|
||||
@@ -73,7 +73,6 @@ export const CookieDropdown = memo(function CookieDropdown() {
|
||||
...(((cookieJars ?? []).length > 1 // Never delete the last one
|
||||
? [
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Delete',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
color: 'danger',
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
FormInputHttpRequest,
|
||||
FormInputSelect,
|
||||
FormInputText,
|
||||
JsonPrimitive,
|
||||
} from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback } from 'react';
|
||||
@@ -15,51 +16,93 @@ import { useFolders } from '../hooks/useFolders';
|
||||
import { useHttpRequests } from '../hooks/useHttpRequests';
|
||||
import { capitalize } from '../lib/capitalize';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Checkbox } from './core/Checkbox';
|
||||
import { Editor } from './core/Editor/Editor';
|
||||
import { Input } from './core/Input';
|
||||
import { Label } from './core/Label';
|
||||
import { Select } from './core/Select';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { Markdown } from './Markdown';
|
||||
import { SelectFile } from './SelectFile';
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const DYNAMIC_FORM_NULL_ARG = '__NULL__';
|
||||
const INPUT_SIZE = 'sm';
|
||||
|
||||
export function DynamicForm<T extends Record<string, string | boolean>>({
|
||||
config,
|
||||
data,
|
||||
onChange,
|
||||
useTemplating,
|
||||
autocompleteVariables,
|
||||
stateKey,
|
||||
}: {
|
||||
config: FormInput[];
|
||||
interface Props<T> {
|
||||
inputs: FormInput[] | undefined | null;
|
||||
onChange: (value: T) => void;
|
||||
data: T;
|
||||
useTemplating?: boolean;
|
||||
autocompleteVariables?: boolean;
|
||||
stateKey: string;
|
||||
}) {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function DynamicForm<T extends Record<string, JsonPrimitive>>({
|
||||
inputs,
|
||||
data,
|
||||
onChange,
|
||||
useTemplating,
|
||||
autocompleteVariables,
|
||||
stateKey,
|
||||
disabled,
|
||||
}: Props<T>) {
|
||||
const setDataAttr = useCallback(
|
||||
(name: string, value: string | boolean | null) => {
|
||||
(name: string, value: JsonPrimitive) => {
|
||||
onChange({ ...data, [name]: value == DYNAMIC_FORM_NULL_ARG ? undefined : value });
|
||||
},
|
||||
[data, onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<FormInputs
|
||||
disabled={disabled}
|
||||
inputs={inputs}
|
||||
setDataAttr={setDataAttr}
|
||||
stateKey={stateKey}
|
||||
useTemplating={useTemplating}
|
||||
autocompleteVariables={autocompleteVariables}
|
||||
data={data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
inputs,
|
||||
autocompleteVariables,
|
||||
stateKey,
|
||||
useTemplating,
|
||||
setDataAttr,
|
||||
data,
|
||||
disabled,
|
||||
}: Pick<Props<T>, 'inputs' | 'useTemplating' | 'autocompleteVariables' | 'stateKey' | 'data'> & {
|
||||
setDataAttr: (name: string, value: JsonPrimitive) => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<VStack space={3} className="h-full overflow-auto">
|
||||
{config.map((a, i) => {
|
||||
switch (a.type) {
|
||||
{inputs?.map((input, i) => {
|
||||
if ('hidden' in input && input.hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ('disabled' in input && disabled != null) {
|
||||
input.disabled = disabled;
|
||||
}
|
||||
|
||||
switch (input.type) {
|
||||
case 'select':
|
||||
return (
|
||||
<SelectArg
|
||||
key={i + stateKey}
|
||||
arg={a}
|
||||
onChange={(v) => setDataAttr(a.name, v)}
|
||||
arg={input}
|
||||
onChange={(v) => setDataAttr(input.name, v)}
|
||||
value={
|
||||
data[a.name] ? String(data[a.name]) : (a.defaultValue ?? DYNAMIC_FORM_NULL_ARG)
|
||||
data[input.name]
|
||||
? String(data[input.name])
|
||||
: (input.defaultValue ?? DYNAMIC_FORM_NULL_ARG)
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -68,11 +111,13 @@ export function DynamicForm<T extends Record<string, string | boolean>>({
|
||||
<TextArg
|
||||
key={i}
|
||||
stateKey={stateKey}
|
||||
arg={a}
|
||||
arg={input}
|
||||
useTemplating={useTemplating || false}
|
||||
autocompleteVariables={autocompleteVariables || false}
|
||||
onChange={(v) => setDataAttr(a.name, v)}
|
||||
value={data[a.name] ? String(data[a.name]) : (a.defaultValue ?? '')}
|
||||
onChange={(v) => setDataAttr(input.name, v)}
|
||||
value={
|
||||
data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? '')
|
||||
}
|
||||
/>
|
||||
);
|
||||
case 'editor':
|
||||
@@ -80,40 +125,79 @@ export function DynamicForm<T extends Record<string, string | boolean>>({
|
||||
<EditorArg
|
||||
key={i}
|
||||
stateKey={stateKey}
|
||||
arg={a}
|
||||
arg={input}
|
||||
useTemplating={useTemplating || false}
|
||||
autocompleteVariables={autocompleteVariables || false}
|
||||
onChange={(v) => setDataAttr(a.name, v)}
|
||||
value={data[a.name] ? String(data[a.name]) : (a.defaultValue ?? '')}
|
||||
onChange={(v) => setDataAttr(input.name, v)}
|
||||
value={
|
||||
data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? '')
|
||||
}
|
||||
/>
|
||||
);
|
||||
case 'checkbox':
|
||||
return (
|
||||
<CheckboxArg
|
||||
key={i + stateKey}
|
||||
arg={a}
|
||||
onChange={(v) => setDataAttr(a.name, v)}
|
||||
value={data[a.name] !== undefined ? data[a.name] === true : false}
|
||||
arg={input}
|
||||
onChange={(v) => setDataAttr(input.name, v)}
|
||||
value={data[input.name] != null ? data[input.name] === true : false}
|
||||
/>
|
||||
);
|
||||
case 'http_request':
|
||||
return (
|
||||
<HttpRequestArg
|
||||
key={i + stateKey}
|
||||
arg={a}
|
||||
onChange={(v) => setDataAttr(a.name, v)}
|
||||
value={data[a.name] ? String(data[a.name]) : DYNAMIC_FORM_NULL_ARG}
|
||||
arg={input}
|
||||
onChange={(v) => setDataAttr(input.name, v)}
|
||||
value={data[input.name] != null ? String(data[input.name]) : DYNAMIC_FORM_NULL_ARG}
|
||||
/>
|
||||
);
|
||||
case 'file':
|
||||
return (
|
||||
<FileArg
|
||||
key={i + stateKey}
|
||||
arg={a}
|
||||
onChange={(v) => setDataAttr(a.name, v)}
|
||||
filePath={data[a.name] ? String(data[a.name]) : DYNAMIC_FORM_NULL_ARG}
|
||||
arg={input}
|
||||
onChange={(v) => setDataAttr(input.name, v)}
|
||||
filePath={
|
||||
data[input.name] != null ? String(data[input.name]) : DYNAMIC_FORM_NULL_ARG
|
||||
}
|
||||
/>
|
||||
);
|
||||
case 'accordion':
|
||||
return (
|
||||
<Banner key={i} className={classNames('!p-0', disabled && 'opacity-disabled')}>
|
||||
<details>
|
||||
<summary className="px-3 py-1.5 text-text-subtle">{input.label}</summary>
|
||||
<div className="mb-3 px-3">
|
||||
<FormInputs
|
||||
data={data}
|
||||
disabled={disabled}
|
||||
inputs={input.inputs}
|
||||
setDataAttr={setDataAttr}
|
||||
stateKey={stateKey}
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
</Banner>
|
||||
);
|
||||
case 'banner':
|
||||
return (
|
||||
<Banner
|
||||
key={i}
|
||||
color={input.color}
|
||||
className={classNames(disabled && 'opacity-disabled')}
|
||||
>
|
||||
<FormInputs
|
||||
data={data}
|
||||
disabled={disabled}
|
||||
inputs={input.inputs}
|
||||
setDataAttr={setDataAttr}
|
||||
stateKey={stateKey}
|
||||
/>
|
||||
</Banner>
|
||||
);
|
||||
case 'markdown':
|
||||
return <Markdown>{input.content}</Markdown>;
|
||||
}
|
||||
})}
|
||||
</VStack>
|
||||
@@ -145,13 +229,17 @@ function TextArg({
|
||||
return (
|
||||
<Input
|
||||
name={arg.name}
|
||||
multiLine={arg.multiLine}
|
||||
onChange={handleChange}
|
||||
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value}
|
||||
required={!arg.optional}
|
||||
disabled={arg.disabled}
|
||||
type={arg.password ? 'password' : 'text'}
|
||||
label={arg.label ?? arg.name}
|
||||
size={INPUT_SIZE}
|
||||
hideLabel={arg.label == null}
|
||||
placeholder={arg.placeholder ?? arg.defaultValue ?? ''}
|
||||
autocomplete={arg.completionOptions ? { options: arg.completionOptions } : undefined}
|
||||
useTemplating={useTemplating}
|
||||
autocompleteVariables={autocompleteVariables}
|
||||
stateKey={stateKey}
|
||||
@@ -184,23 +272,29 @@ function EditorArg({
|
||||
|
||||
const id = `input-${arg.name}`;
|
||||
|
||||
// Read-only editor force refresh for every defaultValue change
|
||||
// Should this be built into the <Editor/> component?
|
||||
const forceUpdateKey = arg.readOnly ? arg.defaultValue + stateKey : stateKey;
|
||||
|
||||
return (
|
||||
<div className=" w-full grid grid-cols-1 grid-rows-[auto_minmax(0,1fr)]">
|
||||
<Label
|
||||
htmlFor={id}
|
||||
optional={arg.optional}
|
||||
required={!arg.optional}
|
||||
visuallyHidden={arg.hideLabel}
|
||||
otherTags={arg.language ? [capitalize(arg.language)] : undefined}
|
||||
tags={arg.language ? [capitalize(arg.language)] : undefined}
|
||||
>
|
||||
{arg.label}
|
||||
</Label>
|
||||
<Editor
|
||||
id={id}
|
||||
className={classNames(
|
||||
'border border-border rounded-md overflow-hidden px-2 py-1.5',
|
||||
'border border-border rounded-md overflow-hidden px-2 py-1',
|
||||
'focus-within:border-border-focus',
|
||||
'max-h-[15rem]', // So it doesn't take up too much space
|
||||
)}
|
||||
autocomplete={arg.completionOptions ? { options: arg.completionOptions } : undefined}
|
||||
disabled={arg.disabled}
|
||||
language={arg.language}
|
||||
onChange={handleChange}
|
||||
heightMode="auto"
|
||||
@@ -209,7 +303,7 @@ function EditorArg({
|
||||
useTemplating={useTemplating}
|
||||
autocompleteVariables={autocompleteVariables}
|
||||
stateKey={stateKey}
|
||||
forceUpdateKey={stateKey}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
hideGutter
|
||||
/>
|
||||
</div>
|
||||
@@ -232,12 +326,9 @@ function SelectArg({
|
||||
onChange={onChange}
|
||||
hideLabel={arg.hideLabel}
|
||||
value={value}
|
||||
options={[
|
||||
...arg.options.map((a) => ({
|
||||
label: a.name,
|
||||
value: a.value,
|
||||
})),
|
||||
]}
|
||||
size={INPUT_SIZE}
|
||||
disabled={arg.disabled}
|
||||
options={arg.options}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -253,6 +344,7 @@ function FileArg({
|
||||
}) {
|
||||
return (
|
||||
<SelectFile
|
||||
disabled={arg.disabled}
|
||||
onChange={({ filePath }) => onChange(filePath)}
|
||||
filePath={filePath === '__NULL__' ? null : filePath}
|
||||
directory={!!arg.directory}
|
||||
@@ -278,6 +370,7 @@ function HttpRequestArg({
|
||||
name={arg.name}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
disabled={arg.disabled}
|
||||
options={[
|
||||
...httpRequests.map((r) => {
|
||||
return {
|
||||
@@ -323,6 +416,7 @@ function CheckboxArg({
|
||||
<Checkbox
|
||||
onChange={onChange}
|
||||
checked={value}
|
||||
disabled={arg.disabled}
|
||||
title={arg.label ?? arg.name}
|
||||
hideLabel={arg.label == null}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Environment } from '@yaakapp-internal/models';
|
||||
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
@@ -11,10 +12,7 @@ import { showPrompt } from '../lib/prompt';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
import { ContextMenu } from './core/Dropdown';
|
||||
import type {
|
||||
GenericCompletionConfig,
|
||||
GenericCompletionOption,
|
||||
} from './core/Editor/genericCompletion';
|
||||
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
|
||||
import { Heading } from './core/Heading';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
@@ -255,7 +253,6 @@ function SidebarButton({
|
||||
onClose={() => setShowContextMenu(null)}
|
||||
items={[
|
||||
{
|
||||
key: 'rename',
|
||||
label: 'Rename',
|
||||
leftSlot: <Icon icon="pencil" size="sm" />,
|
||||
onSelect: async () => {
|
||||
@@ -277,7 +274,6 @@ function SidebarButton({
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete-environment',
|
||||
color: 'danger',
|
||||
label: 'Delete',
|
||||
leftSlot: <Icon icon="trash" size="sm" />,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { emit } from '@tauri-apps/api/event';
|
||||
import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugins';
|
||||
import type { InternalEvent } from '@yaakapp-internal/plugins';
|
||||
import type { ShowToastRequest } from '@yaakapp/api';
|
||||
import { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace';
|
||||
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
|
||||
@@ -12,6 +12,7 @@ import { useSyncModelStores } from '../hooks/useSyncModelStores';
|
||||
import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels';
|
||||
import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting';
|
||||
import { useSubscribeTemplateFunctions } from '../hooks/useTemplateFunctions';
|
||||
import { generateId } from '../lib/generateId';
|
||||
import { showPrompt } from '../lib/prompt';
|
||||
import { showToast } from '../lib/toast';
|
||||
|
||||
@@ -36,15 +37,24 @@ export function GlobalHooks() {
|
||||
showToast({ ...event.payload });
|
||||
});
|
||||
|
||||
// Listen for prompts
|
||||
useListenToTauriEvent<{ replyId: string; args: PromptTextRequest }>(
|
||||
'show_prompt',
|
||||
async (event) => {
|
||||
const value = await showPrompt(event.payload.args);
|
||||
const result: PromptTextResponse = { value };
|
||||
await emit(event.payload.replyId, result);
|
||||
},
|
||||
);
|
||||
// Listen for plugin events
|
||||
useListenToTauriEvent<InternalEvent>('plugin_event', async ({ payload: event }) => {
|
||||
if (event.payload.type === 'prompt_text_request') {
|
||||
const value = await showPrompt(event.payload);
|
||||
const result: InternalEvent = {
|
||||
id: generateId(),
|
||||
replyId: event.id,
|
||||
pluginName: event.pluginName,
|
||||
pluginRefId: event.pluginRefId,
|
||||
windowContext: event.windowContext,
|
||||
payload: {
|
||||
type: 'prompt_text_response',
|
||||
value,
|
||||
},
|
||||
};
|
||||
await emit(event.id, result);
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -69,13 +69,11 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
key: 'refresh',
|
||||
label: 'Refetch',
|
||||
leftSlot: <Icon icon="refresh" />,
|
||||
onSelect: refetch,
|
||||
},
|
||||
{
|
||||
key: 'clear',
|
||||
label: 'Clear',
|
||||
onSelect: clear,
|
||||
hidden: !schema,
|
||||
@@ -84,7 +82,6 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
|
||||
},
|
||||
{ type: 'separator', label: 'Setting' },
|
||||
{
|
||||
key: 'auto_fetch',
|
||||
label: 'Automatic Introspection',
|
||||
onSelect: () => {
|
||||
setAutoIntrospectDisabled({
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { CSSProperties } from 'react';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { useContainerSize } from '../hooks/useContainerQuery';
|
||||
import type { ReflectResponseService } from '../hooks/useGrpc';
|
||||
import { useHttpAuthentication } from '../hooks/useHttpAuthentication';
|
||||
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
@@ -69,7 +69,7 @@ export function GrpcConnectionSetupPane({
|
||||
onSend,
|
||||
}: Props) {
|
||||
const updateRequest = useUpdateAnyGrpcRequest();
|
||||
const authentication = useHttpAuthentication();
|
||||
const authentication = useHttpAuthenticationSummaries();
|
||||
const [activeTabs, setActiveTabs] = useAtom(tabsAtom);
|
||||
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
|
||||
|
||||
@@ -237,7 +237,6 @@ export function GrpcConnectionSetupPane({
|
||||
{
|
||||
label: 'Refresh',
|
||||
type: 'default',
|
||||
key: 'custom',
|
||||
leftSlot: <Icon className="text-text-subtlest" size="sm" icon="refresh" />,
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
|
||||
import { charsets } from '../lib/data/charsets';
|
||||
import { connections } from '../lib/data/connections';
|
||||
import { encodings } from '../lib/data/encodings';
|
||||
@@ -44,7 +45,7 @@ const headerOptionsMap: Record<string, string[]> = {
|
||||
|
||||
const valueAutocomplete = (headerName: string): GenericCompletionConfig | undefined => {
|
||||
const name = headerName.toLowerCase().trim();
|
||||
const options: GenericCompletionConfig['options'] =
|
||||
const options: GenericCompletionOption[] =
|
||||
headerOptionsMap[name]?.map((o) => ({
|
||||
label: o,
|
||||
type: 'constant',
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useHttpAuthentication } from '../hooks/useHttpAuthentication';
|
||||
import { useHttpAuthenticationConfig } from '../hooks/useHttpAuthenticationConfig';
|
||||
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
|
||||
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
||||
import { Checkbox } from './core/Checkbox';
|
||||
import type { DropdownItem } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { DynamicForm } from './DynamicForm';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
|
||||
@@ -13,11 +19,15 @@ interface Props {
|
||||
export function HttpAuthenticationEditor({ request }: Props) {
|
||||
const updateHttpRequest = useUpdateAnyHttpRequest();
|
||||
const updateGrpcRequest = useUpdateAnyGrpcRequest();
|
||||
const auths = useHttpAuthentication();
|
||||
const auth = auths.find((a) => a.name === request.authenticationType);
|
||||
const auth = useHttpAuthenticationConfig(
|
||||
request.authenticationType,
|
||||
request.authentication,
|
||||
request.id,
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(authentication: Record<string, boolean>) => {
|
||||
console.log('UPDATE', authentication);
|
||||
if (request.model === 'http_request') {
|
||||
updateHttpRequest.mutate({
|
||||
id: request.id,
|
||||
@@ -33,18 +43,42 @@ export function HttpAuthenticationEditor({ request }: Props) {
|
||||
[request.id, request.model, updateGrpcRequest, updateHttpRequest],
|
||||
);
|
||||
|
||||
if (auth == null) {
|
||||
if (auth.data == null) {
|
||||
return <EmptyStateText>No Authentication {request.authenticationType}</EmptyStateText>;
|
||||
}
|
||||
|
||||
return (
|
||||
<DynamicForm
|
||||
autocompleteVariables
|
||||
useTemplating
|
||||
stateKey={`auth.${request.id}.${request.authenticationType}`}
|
||||
config={auth.config}
|
||||
data={request.authentication}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
|
||||
<HStack space={2} className="mb-1" alignItems="center">
|
||||
<Checkbox
|
||||
className="w-full"
|
||||
checked={!request.authentication.disabled}
|
||||
onChange={(disabled) => handleChange({ ...request.authentication, disabled: !disabled })}
|
||||
title="Enabled"
|
||||
/>
|
||||
{auth.data.actions && (
|
||||
<Dropdown
|
||||
items={auth.data.actions.map(
|
||||
(a): DropdownItem => ({
|
||||
label: a.label,
|
||||
leftSlot: a.icon ? <Icon icon={a.icon} /> : null,
|
||||
onSelect: () => a.call(request),
|
||||
}),
|
||||
)}
|
||||
>
|
||||
<IconButton title="Authentication Actions" icon="settings" size="xs" />
|
||||
</Dropdown>
|
||||
)}
|
||||
</HStack>
|
||||
<DynamicForm
|
||||
disabled={request.authentication.disabled}
|
||||
autocompleteVariables
|
||||
useTemplating
|
||||
stateKey={`auth.${request.id}.${request.authenticationType}`}
|
||||
inputs={auth.data.args}
|
||||
data={request.authentication}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
27
src-web/components/Markdown.tsx
Normal file
27
src-web/components/Markdown.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import ReactMarkdown, { type Components } from 'react-markdown';
|
||||
import { Prose } from './Prose';
|
||||
|
||||
export function Markdown({ children, className }: { children: string; className?: string }) {
|
||||
return (
|
||||
<Prose className={className}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
</Prose>
|
||||
);
|
||||
}
|
||||
|
||||
const markdownComponents: Partial<Components> = {
|
||||
// Ensure links open in external browser by adding target="_blank"
|
||||
a: ({ href, children, ...rest }) => {
|
||||
if (href && !href.match(/https?:\/\//)) {
|
||||
href = `http://${href}`;
|
||||
}
|
||||
return (
|
||||
<a target="_blank" rel="noreferrer noopener" href={href} {...rest}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -1,12 +1,9 @@
|
||||
import classNames from 'classnames';
|
||||
import { useRef, useState } from 'react';
|
||||
import type { Components } from 'react-markdown';
|
||||
import Markdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import type { EditorProps } from './core/Editor/Editor';
|
||||
import { Editor } from './core/Editor/Editor';
|
||||
import { Prose } from './Prose';
|
||||
import { SegmentedControl } from './core/SegmentedControl';
|
||||
import { Markdown } from './Markdown';
|
||||
|
||||
type ViewMode = 'edit' | 'preview';
|
||||
|
||||
@@ -47,11 +44,9 @@ export function MarkdownEditor({
|
||||
defaultValue.length === 0 ? (
|
||||
<p className="text-text-subtlest">No description</p>
|
||||
) : (
|
||||
<Prose className="max-w-xl overflow-y-auto max-h-full [&_*]:cursor-auto [&_*]:select-auto">
|
||||
<Markdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
{defaultValue}
|
||||
</Markdown>
|
||||
</Prose>
|
||||
<Markdown className="max-w-xl overflow-y-auto max-h-full [&_*]:cursor-auto [&_*]:select-auto">
|
||||
{defaultValue}
|
||||
</Markdown>
|
||||
);
|
||||
|
||||
const contents = viewMode === 'preview' ? preview : editor;
|
||||
@@ -88,17 +83,3 @@ export function MarkdownEditor({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const markdownComponents: Partial<Components> = {
|
||||
// Ensure links open in external browser by adding target="_blank"
|
||||
a: ({ href, children, ...rest }) => {
|
||||
if (href && !href.match(/https?:\/\//)) {
|
||||
href = `http://${href}`;
|
||||
}
|
||||
return (
|
||||
<a target="_blank" rel="noreferrer noopener" href={href} {...rest}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
@apply mt-0;
|
||||
}
|
||||
|
||||
& > :last-child {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
img,
|
||||
video,
|
||||
p,
|
||||
@@ -107,6 +111,7 @@
|
||||
ul code {
|
||||
@apply text-xs bg-surface-active text-info font-normal whitespace-nowrap;
|
||||
@apply px-1.5 py-0.5 rounded not-italic;
|
||||
@apply select-text;
|
||||
}
|
||||
|
||||
pre {
|
||||
|
||||
@@ -27,13 +27,11 @@ export function RecentConnectionsDropdown({
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
key: 'clear-single',
|
||||
label: 'Clear Connection',
|
||||
onSelect: deleteConnection.mutate,
|
||||
disabled: connections.length === 0,
|
||||
},
|
||||
{
|
||||
key: 'clear-all',
|
||||
label: `Clear ${pluralizeCount('Connection', connections.length)}`,
|
||||
onSelect: deleteAllConnections.mutate,
|
||||
hidden: connections.length <= 1,
|
||||
@@ -41,7 +39,6 @@ export function RecentConnectionsDropdown({
|
||||
},
|
||||
{ type: 'separator', label: 'History' },
|
||||
...connections.slice(0, 20).map((c) => ({
|
||||
key: c.id,
|
||||
label: (
|
||||
<HStack space={2}>
|
||||
{formatDistanceToNowStrict(c.createdAt + 'Z')} ago •{' '}
|
||||
|
||||
@@ -58,7 +58,6 @@ export function RecentRequestsDropdown({ className }: Props) {
|
||||
if (request === undefined) continue;
|
||||
|
||||
recentRequestItems.push({
|
||||
key: request.id,
|
||||
label: fallbackRequestName(request),
|
||||
// leftSlot: <CountBadge className="!ml-0 px-0 w-5" count={recentRequestItems.length} />,
|
||||
leftSlot: <HttpMethodTag className="text-right" shortNames request={request} />,
|
||||
|
||||
@@ -32,7 +32,6 @@ export const RecentResponsesDropdown = function ResponsePane({
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
key: 'save',
|
||||
label: 'Save to File',
|
||||
onSelect: saveResponse.mutate,
|
||||
leftSlot: <Icon icon="save" />,
|
||||
@@ -40,7 +39,6 @@ export const RecentResponsesDropdown = function ResponsePane({
|
||||
disabled: activeResponse.state !== 'closed' && activeResponse.status >= 100,
|
||||
},
|
||||
{
|
||||
key: 'copy',
|
||||
label: 'Copy Body',
|
||||
onSelect: copyResponse.mutate,
|
||||
leftSlot: <Icon icon="copy" />,
|
||||
@@ -48,13 +46,11 @@ export const RecentResponsesDropdown = function ResponsePane({
|
||||
disabled: activeResponse.state !== 'closed' && activeResponse.status >= 100,
|
||||
},
|
||||
{
|
||||
key: 'clear-single',
|
||||
label: 'Delete',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
onSelect: deleteResponse.mutate,
|
||||
},
|
||||
{
|
||||
key: 'unpin',
|
||||
label: 'Unpin Response',
|
||||
onSelect: () => onPinnedResponseId(activeResponse.id),
|
||||
leftSlot: <Icon icon="unpin" />,
|
||||
@@ -63,7 +59,6 @@ export const RecentResponsesDropdown = function ResponsePane({
|
||||
},
|
||||
{ type: 'separator', label: 'History' },
|
||||
{
|
||||
key: 'clear-all',
|
||||
label: `Delete ${responses.length} ${pluralize('Response', responses.length)}`,
|
||||
onSelect: deleteAllResponses.mutate,
|
||||
hidden: responses.length === 0,
|
||||
@@ -71,7 +66,6 @@ export const RecentResponsesDropdown = function ResponsePane({
|
||||
},
|
||||
{ type: 'separator' },
|
||||
...responses.slice(0, 20).map((r: HttpResponse) => ({
|
||||
key: r.id,
|
||||
label: (
|
||||
<HStack space={2}>
|
||||
<StatusTag className="text-sm" response={r} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
import type {GenericCompletionOption} from "@yaakapp-internal/plugins";
|
||||
import classNames from 'classnames';
|
||||
import { atom, useAtom, useAtomValue } from 'jotai';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
@@ -8,7 +9,7 @@ import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
|
||||
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
|
||||
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
|
||||
import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
|
||||
import { useHttpAuthentication } from '../hooks/useHttpAuthentication';
|
||||
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
|
||||
import { httpRequestsAtom } from '../hooks/useHttpRequests';
|
||||
import { useImportCurl } from '../hooks/useImportCurl';
|
||||
import { useImportQuerystring } from '../hooks/useImportQuerystring';
|
||||
@@ -36,10 +37,7 @@ import { showToast } from '../lib/toast';
|
||||
import { BinaryFileEditor } from './BinaryFileEditor';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
import { Editor } from './core/Editor/Editor';
|
||||
import type {
|
||||
GenericCompletionConfig,
|
||||
GenericCompletionOption,
|
||||
} from './core/Editor/genericCompletion';
|
||||
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import type { Pair } from './core/PairEditor';
|
||||
import { PlainInput } from './core/PlainInput';
|
||||
@@ -93,7 +91,7 @@ export const RequestPane = memo(function RequestPane({
|
||||
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
|
||||
const [{ urlKey }] = useRequestEditor();
|
||||
const contentType = useContentTypeFromHeaders(activeRequest.headers);
|
||||
const authentication = useHttpAuthentication();
|
||||
const authentication = useHttpAuthenticationSummaries();
|
||||
|
||||
const handleContentTypeChange = useCallback(
|
||||
async (contentType: string | null) => {
|
||||
|
||||
@@ -28,14 +28,12 @@ export function SettingsDropdown() {
|
||||
ref={dropdownRef}
|
||||
items={[
|
||||
{
|
||||
key: 'settings',
|
||||
label: 'Settings',
|
||||
hotKeyAction: 'settings.show',
|
||||
leftSlot: <Icon icon="settings" />,
|
||||
onSelect: openSettings.mutate,
|
||||
},
|
||||
{
|
||||
key: 'hotkeys',
|
||||
label: 'Keyboard shortcuts',
|
||||
hotKeyAction: 'hotkeys.showHelp',
|
||||
leftSlot: <Icon icon="keyboard" />,
|
||||
@@ -49,33 +47,28 @@ export function SettingsDropdown() {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'import-data',
|
||||
label: 'Import Data',
|
||||
leftSlot: <Icon icon="folder_input" />,
|
||||
onSelect: () => importData.mutate(),
|
||||
},
|
||||
{
|
||||
key: 'export-data',
|
||||
label: 'Export Data',
|
||||
leftSlot: <Icon icon="folder_output" />,
|
||||
onSelect: () => exportData.mutate(),
|
||||
},
|
||||
{ type: 'separator', label: `Yaak v${appInfo.version}` },
|
||||
{
|
||||
key: 'update-check',
|
||||
label: 'Check for Updates',
|
||||
leftSlot: <Icon icon="update" />,
|
||||
onSelect: () => checkForUpdates.mutate(),
|
||||
},
|
||||
{
|
||||
key: 'feedback',
|
||||
label: 'Feedback',
|
||||
leftSlot: <Icon icon="chat" />,
|
||||
rightSlot: <Icon icon="external_link" />,
|
||||
onSelect: () => openUrl('https://yaak.app/roadmap'),
|
||||
},
|
||||
{
|
||||
key: 'changelog',
|
||||
label: 'Changelog',
|
||||
leftSlot: <Icon icon="cake" />,
|
||||
rightSlot: <Icon icon="external_link" />,
|
||||
|
||||
@@ -46,13 +46,11 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
|
||||
if (child.model === 'folder') {
|
||||
return [
|
||||
{
|
||||
key: 'send-all',
|
||||
label: 'Send All',
|
||||
leftSlot: <Icon icon="send_horizontal" />,
|
||||
onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.id)),
|
||||
},
|
||||
{
|
||||
key: 'folder-settings',
|
||||
label: 'Settings',
|
||||
leftSlot: <Icon icon="settings" />,
|
||||
onSelect: () =>
|
||||
@@ -64,13 +62,11 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: 'duplicateFolder',
|
||||
label: 'Duplicate',
|
||||
leftSlot: <Icon icon="copy" />,
|
||||
onSelect: () => duplicateFolder.mutate(),
|
||||
},
|
||||
{
|
||||
key: 'delete-folder',
|
||||
label: 'Delete',
|
||||
color: 'danger',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
@@ -84,7 +80,6 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
|
||||
child.model === 'http_request'
|
||||
? [
|
||||
{
|
||||
key: 'send-request',
|
||||
label: 'Send',
|
||||
hotKeyAction: 'http_request.send',
|
||||
hotKeyLabelOnly: true, // Already bound in URL bar
|
||||
@@ -92,7 +87,6 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
|
||||
onSelect: () => sendRequest.mutate(child.id),
|
||||
},
|
||||
...httpRequestActions.map((a) => ({
|
||||
key: a.key,
|
||||
label: a.label,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
leftSlot: <Icon icon={(a.icon as any) ?? 'empty'} />,
|
||||
@@ -107,13 +101,11 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
|
||||
return [
|
||||
...requestItems,
|
||||
{
|
||||
key: 'rename-request',
|
||||
label: 'Rename',
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
onSelect: renameRequest.mutate,
|
||||
},
|
||||
{
|
||||
key: 'duplicate-request',
|
||||
label: 'Duplicate',
|
||||
hotKeyAction: 'http_request.duplicate',
|
||||
hotKeyLabelOnly: true, // Would trigger for every request (bad)
|
||||
@@ -124,14 +116,12 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
|
||||
: duplicateGrpcRequest.mutate(),
|
||||
},
|
||||
{
|
||||
key: 'move-workspace',
|
||||
label: 'Move',
|
||||
leftSlot: <Icon icon="arrow_right_circle" />,
|
||||
hidden: workspaces.length <= 1,
|
||||
onSelect: moveToWorkspace.mutate,
|
||||
},
|
||||
{
|
||||
key: 'delete-request',
|
||||
color: 'danger',
|
||||
label: 'Delete',
|
||||
hotKeyAction: 'http_request.delete',
|
||||
|
||||
@@ -25,6 +25,10 @@ export function TemplateFunctionDialog({ templateFunction, hide, initialTokens,
|
||||
? initialTokens.tokens[0]?.val.args
|
||||
: [];
|
||||
for (const arg of templateFunction.args) {
|
||||
if (!('name' in arg)) {
|
||||
// Skip visual-only args
|
||||
continue;
|
||||
}
|
||||
const initialArg = initialArgs.find((a) => a.name === arg.name);
|
||||
const initialArgValue =
|
||||
initialArg?.value.type === 'str'
|
||||
@@ -79,7 +83,7 @@ export function TemplateFunctionDialog({ templateFunction, hide, initialTokens,
|
||||
<VStack className="pb-3" space={4}>
|
||||
<h1 className="font-mono !text-base">{templateFunction.name}(…)</h1>
|
||||
<DynamicForm
|
||||
config={templateFunction.args}
|
||||
inputs={templateFunction.args}
|
||||
data={argValues}
|
||||
onChange={setArgValues}
|
||||
stateKey={`template_function.${templateFunction.name}`}
|
||||
|
||||
@@ -46,7 +46,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
|
||||
const extraItems: DropdownItem[] = [
|
||||
{
|
||||
key: 'workspace-settings',
|
||||
label: 'Workspace Settings',
|
||||
leftSlot: <Icon icon="settings" />,
|
||||
hotKeyAction: 'workspace_settings.show',
|
||||
@@ -62,7 +61,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'reveal-workspace-sync-dir',
|
||||
label: revealInFinderText,
|
||||
hidden: workspaceMeta == null || workspaceMeta.settingSyncDir == null,
|
||||
leftSlot: <Icon icon="folder_open" />,
|
||||
@@ -72,7 +70,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete-responses',
|
||||
label: 'Clear Send History',
|
||||
color: 'warning',
|
||||
leftSlot: <Icon icon="history" />,
|
||||
@@ -80,13 +77,11 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
key: 'create-workspace',
|
||||
label: 'New Workspace',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: createWorkspace,
|
||||
},
|
||||
{
|
||||
key: 'open-workspace',
|
||||
label: 'Open Workspace',
|
||||
leftSlot: <Icon icon="folder" />,
|
||||
onSelect: openWorkspaceFromSyncDir.mutate,
|
||||
|
||||
@@ -7,7 +7,7 @@ interface Props {
|
||||
color?: 'primary' | 'secondary' | 'success' | 'notice' | 'warning' | 'danger' | 'info';
|
||||
}
|
||||
|
||||
export function Banner({ children, className, color = 'secondary' }: Props) {
|
||||
export function Banner({ children, className, color }: Props) {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
@@ -16,7 +16,7 @@ export function Banner({ children, className, color = 'secondary' }: Props) {
|
||||
`x-theme-banner--${color}`,
|
||||
'whitespace-pre-wrap',
|
||||
'border border-dashed border-border bg-surface',
|
||||
'px-3 py-2 rounded select-auto cursor-text',
|
||||
'px-3 py-2 rounded select-auto',
|
||||
'overflow-x-auto text-text',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { type ReactNode } from 'react';
|
||||
import { trackEvent } from '../../lib/analytics';
|
||||
import { Icon } from './Icon';
|
||||
import { HStack } from './Stacks';
|
||||
@@ -26,17 +26,15 @@ export function Checkbox({
|
||||
event,
|
||||
}: CheckboxProps) {
|
||||
return (
|
||||
<HStack
|
||||
as="label"
|
||||
space={2}
|
||||
className={classNames(className, 'text-text mr-auto', disabled && 'opacity-disabled')}
|
||||
>
|
||||
<HStack as="label" space={2} className={classNames(className, 'text-text mr-auto')}>
|
||||
<div className={classNames(inputWrapperClassName, 'x-theme-input', 'relative flex')}>
|
||||
<input
|
||||
aria-hidden
|
||||
className={classNames(
|
||||
'appearance-none w-4 h-4 flex-shrink-0 border border-border',
|
||||
'rounded hocus:border-border-focus hocus:bg-focus/[5%] outline-none ring-0',
|
||||
'rounded outline-none ring-0',
|
||||
!disabled && 'hocus:border-border-focus hocus:bg-focus/[5%] ',
|
||||
disabled && 'border-dotted',
|
||||
)}
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
@@ -54,7 +52,7 @@ export function Checkbox({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!hideLabel && title}
|
||||
<span className={classNames(disabled && 'opacity-disabled')}>{!hideLabel && title}</span>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ export type DropdownItemSeparator = {
|
||||
};
|
||||
|
||||
export type DropdownItemDefault = {
|
||||
key: string;
|
||||
type?: 'default';
|
||||
label: ReactNode;
|
||||
keepOpen?: boolean;
|
||||
@@ -465,11 +464,12 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
return (
|
||||
<>
|
||||
{items.map(
|
||||
(item) =>
|
||||
(item, i) =>
|
||||
item.type !== 'separator' &&
|
||||
!item.hotKeyLabelOnly && (
|
||||
!item.hotKeyLabelOnly &&
|
||||
item.hotKeyAction && (
|
||||
<MenuItemHotKey
|
||||
key={item.key}
|
||||
key={`${item.hotKeyAction}::${i}`}
|
||||
onSelect={handleSelect}
|
||||
item={item}
|
||||
action={item.hotKeyAction}
|
||||
@@ -542,7 +542,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
focused={i === selectedIndex}
|
||||
onFocus={handleFocus}
|
||||
onSelect={handleSelect}
|
||||
key={item.key}
|
||||
key={`item_${i}`}
|
||||
item={item}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
@apply w-full block text-base;
|
||||
|
||||
/* Regular cursor */
|
||||
|
||||
.cm-cursor {
|
||||
@apply border-text !important;
|
||||
/* Widen the cursor a bit */
|
||||
@@ -12,6 +13,7 @@
|
||||
}
|
||||
|
||||
/* Vim-mode cursor */
|
||||
|
||||
.cm-fat-cursor {
|
||||
@apply outline-0 bg-text !important;
|
||||
@apply text-surface !important;
|
||||
@@ -181,7 +183,7 @@
|
||||
@apply hidden !important;
|
||||
}
|
||||
|
||||
&.cm-singleline .cm-line {
|
||||
&.cm-singleline * {
|
||||
@apply cursor-default;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defaultKeymap, historyField } from '@codemirror/commands';
|
||||
import { defaultKeymap, historyField, indentWithTab } from '@codemirror/commands';
|
||||
import { foldState, forceParsing } from '@codemirror/language';
|
||||
import type { EditorStateConfig, Extension } from '@codemirror/state';
|
||||
import { Compartment, EditorState } from '@codemirror/state';
|
||||
@@ -34,14 +34,22 @@ import { TemplateVariableDialog } from '../../TemplateVariableDialog';
|
||||
import { IconButton } from '../IconButton';
|
||||
import { HStack } from '../Stacks';
|
||||
import './Editor.css';
|
||||
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
|
||||
import {
|
||||
baseExtensions,
|
||||
getLanguageExtension,
|
||||
multiLineExtensions,
|
||||
readonlyExtensions,
|
||||
} from './extensions';
|
||||
import type { GenericCompletionConfig } from './genericCompletion';
|
||||
import { singleLineExtensions } from './singleLine';
|
||||
|
||||
// VSCode's Tab actions mess with the single-line editor tab actions, so remove it.
|
||||
const vsCodeWithoutTab = vscodeKeymap.filter((k) => k.key !== 'Tab');
|
||||
|
||||
const keymapExtensions: Record<EditorKeymap, Extension> = {
|
||||
vim: vim(),
|
||||
emacs: emacs(),
|
||||
vscode: keymap.of(vscodeKeymap),
|
||||
vscode: keymap.of(vsCodeWithoutTab),
|
||||
default: [],
|
||||
};
|
||||
|
||||
@@ -68,6 +76,7 @@ export interface EditorProps {
|
||||
onKeyDown?: (e: KeyboardEvent) => void;
|
||||
singleLine?: boolean;
|
||||
wrapLines?: boolean;
|
||||
disableTabIndent?: boolean;
|
||||
format?: (v: string) => Promise<string>;
|
||||
autocomplete?: GenericCompletionConfig;
|
||||
autocompleteVariables?: boolean;
|
||||
@@ -85,9 +94,9 @@ const emptyExtension: Extension = [];
|
||||
export const Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
{
|
||||
readOnly,
|
||||
type = 'text',
|
||||
type,
|
||||
heightMode,
|
||||
language = 'text',
|
||||
language,
|
||||
autoFocus,
|
||||
autoSelect,
|
||||
placeholder,
|
||||
@@ -101,6 +110,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
className,
|
||||
disabled,
|
||||
singleLine,
|
||||
format,
|
||||
autocomplete,
|
||||
@@ -108,6 +118,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
autocompleteVariables,
|
||||
actions,
|
||||
wrapLines,
|
||||
disableTabIndent,
|
||||
hideGutter,
|
||||
stateKey,
|
||||
}: EditorProps,
|
||||
@@ -122,6 +133,20 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
wrapLines = settings.editorSoftWrap;
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
readOnly = true;
|
||||
}
|
||||
|
||||
if (
|
||||
singleLine ||
|
||||
language == null ||
|
||||
language === 'text' ||
|
||||
language === 'url' ||
|
||||
language === 'pairs'
|
||||
) {
|
||||
disableTabIndent = true;
|
||||
}
|
||||
|
||||
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
|
||||
useImperativeHandle(ref, () => cm.current?.view, []);
|
||||
|
||||
@@ -166,7 +191,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
useEffect(
|
||||
function configurePlaceholder() {
|
||||
if (cm.current === null) return;
|
||||
const ext = placeholderExt(placeholderElFromText(placeholder ?? '', type));
|
||||
const ext = placeholderExt(placeholderElFromText(placeholder, type));
|
||||
const effects = placeholderCompartment.current.reconfigure(ext);
|
||||
cm.current?.view.dispatch({ effects });
|
||||
},
|
||||
@@ -209,6 +234,23 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
[wrapLines],
|
||||
);
|
||||
|
||||
// Update tab indent
|
||||
const tabIndentCompartment = useRef(new Compartment());
|
||||
useEffect(
|
||||
function configureTabIndent() {
|
||||
if (cm.current === null) return;
|
||||
const current = tabIndentCompartment.current.get(cm.current.view.state) ?? emptyExtension;
|
||||
// PERF: This is expensive with hundreds of editors on screen, so only do it when necessary
|
||||
if (disableTabIndent && current !== emptyExtension) return; // Nothing to do
|
||||
if (!disableTabIndent && current === emptyExtension) return; // Nothing to do
|
||||
|
||||
const ext = !disableTabIndent ? keymap.of([indentWithTab]) : emptyExtension;
|
||||
const effects = tabIndentCompartment.current.reconfigure(ext);
|
||||
cm.current?.view.dispatch({ effects });
|
||||
},
|
||||
[disableTabIndent],
|
||||
);
|
||||
|
||||
const onClickFunction = useCallback(
|
||||
async (fn: TemplateFunction, tagValue: string, startPos: number) => {
|
||||
const initialTokens = await parseTemplate(tagValue);
|
||||
@@ -342,9 +384,12 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
const extensions = [
|
||||
languageCompartment.of(langExt),
|
||||
placeholderCompartment.current.of(
|
||||
placeholderExt(placeholderElFromText(placeholder ?? '', type)),
|
||||
placeholderExt(placeholderElFromText(placeholder, type)),
|
||||
),
|
||||
wrapLinesCompartment.current.of(wrapLines ? EditorView.lineWrapping : emptyExtension),
|
||||
tabIndentCompartment.current.of(
|
||||
!disableTabIndent ? keymap.of([indentWithTab]) : emptyExtension,
|
||||
),
|
||||
keymapCompartment.current.of(
|
||||
keymapExtensions[settings.editorKeymap] ?? keymapExtensions['default'],
|
||||
),
|
||||
@@ -475,6 +520,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
className={classNames(
|
||||
className,
|
||||
'cm-wrapper text-base',
|
||||
disabled && 'opacity-disabled',
|
||||
type === 'password' && 'cm-obscure-text',
|
||||
heightMode === 'auto' ? 'cm-auto-height' : 'cm-full-height',
|
||||
singleLine ? 'cm-singleline' : 'cm-multiline',
|
||||
@@ -557,10 +603,8 @@ function getExtensions({
|
||||
tooltips({ parent }),
|
||||
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap),
|
||||
...(singleLine ? [singleLineExtensions()] : []),
|
||||
...(!singleLine ? [multiLineExtensions({ hideGutter })] : []),
|
||||
...(readOnly
|
||||
? [EditorState.readOnly.of(true), EditorView.contentAttributes.of({ tabindex: '-1' })]
|
||||
: []),
|
||||
...(!singleLine ? multiLineExtensions({ hideGutter }) : []),
|
||||
...(readOnly ? readonlyExtensions : []),
|
||||
|
||||
// ------------------------ //
|
||||
// Things that must be last //
|
||||
@@ -580,13 +624,15 @@ function getExtensions({
|
||||
];
|
||||
}
|
||||
|
||||
const placeholderElFromText = (text: string, type: EditorProps['type']) => {
|
||||
const placeholderElFromText = (text: string | undefined, type: EditorProps['type']) => {
|
||||
const el = document.createElement('div');
|
||||
if (type === 'password') {
|
||||
// Will be obscured (dots) so just needs to be something to take up space
|
||||
el.innerHTML = 'something-cool';
|
||||
el.setAttribute('aria-hidden', 'true');
|
||||
} else {
|
||||
// Default to <SPACE> because codemirror needs it for sizing. I'm not sure why, but probably something
|
||||
// to do with how Yaak "hacks" it with CSS for single line input.
|
||||
el.innerHTML = text ? text.replaceAll('\n', '<br/>') : ' ';
|
||||
}
|
||||
return el;
|
||||
@@ -596,8 +642,8 @@ function saveCachedEditorState(stateKey: string | null, state: EditorState | nul
|
||||
if (!stateKey || state == null) return;
|
||||
const stateObj = state.toJSON(stateFields);
|
||||
|
||||
// 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
|
||||
// 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;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
closeBracketsKeymap,
|
||||
completionKeymap,
|
||||
} from '@codemirror/autocomplete';
|
||||
import { history, historyKeymap, indentWithTab } from '@codemirror/commands';
|
||||
import { history, historyKeymap } from '@codemirror/commands';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
@@ -142,6 +142,11 @@ export const baseExtensions = [
|
||||
keymap.of([...historyKeymap, ...completionKeymap]),
|
||||
];
|
||||
|
||||
export const readonlyExtensions = [
|
||||
EditorState.readOnly.of(true),
|
||||
EditorView.contentAttributes.of({ tabindex: '-1' }),
|
||||
];
|
||||
|
||||
export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) => [
|
||||
hideGutter
|
||||
? []
|
||||
@@ -208,5 +213,5 @@ export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) =>
|
||||
rectangularSelection(),
|
||||
crosshairCursor(),
|
||||
highlightActiveLineGutter(),
|
||||
keymap.of([indentWithTab, ...closeBracketsKeymap, ...searchKeymap, ...foldKeymap, ...lintKeymap]),
|
||||
keymap.of([...closeBracketsKeymap, ...searchKeymap, ...foldKeymap, ...lintKeymap]),
|
||||
];
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
import type { CompletionContext } from '@codemirror/autocomplete';
|
||||
|
||||
export interface GenericCompletionOption {
|
||||
label: string;
|
||||
type: 'constant' | 'variable';
|
||||
detail?: string;
|
||||
info?: string;
|
||||
/** When given, should be a number from -99 to 99 that adjusts
|
||||
* how this completion is ranked compared to other completions
|
||||
* that match the input as well as this one. A negative number
|
||||
* moves it down the list, a positive number moves it up. */
|
||||
boost?: number;
|
||||
}
|
||||
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
|
||||
|
||||
export interface GenericCompletionConfig {
|
||||
minMatch?: number;
|
||||
|
||||
@@ -16,6 +16,7 @@ export type InputProps = Pick<
|
||||
| 'useTemplating'
|
||||
| 'autocomplete'
|
||||
| 'forceUpdateKey'
|
||||
| 'disabled'
|
||||
| 'autoFocus'
|
||||
| 'autoSelect'
|
||||
| 'autocompleteVariables'
|
||||
@@ -75,6 +76,7 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
|
||||
readOnly,
|
||||
stateKey,
|
||||
multiLine,
|
||||
disabled,
|
||||
...props
|
||||
}: InputProps,
|
||||
ref,
|
||||
@@ -82,18 +84,26 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
|
||||
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
|
||||
const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [hasChanged, setHasChanged] = useStateWithDeps<boolean>(false, [stateKey, forceUpdateKey]);
|
||||
const editorRef = useRef<EditorView | null>(null);
|
||||
useImperativeHandle<EditorView | null, EditorView | null>(ref, () => editorRef.current);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
if (readOnly) return;
|
||||
setFocused(true);
|
||||
// Select all text on focus
|
||||
editorRef.current?.dispatch({
|
||||
selection: { anchor: 0, head: editorRef.current.state.doc.length },
|
||||
});
|
||||
onFocus?.();
|
||||
}, [onFocus, readOnly]);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
setFocused(false);
|
||||
editorRef.current?.dispatch({ selection: { anchor: 0 } });
|
||||
// Move selection to the end on blur
|
||||
editorRef.current?.dispatch({
|
||||
selection: { anchor: editorRef.current.state.doc.length },
|
||||
});
|
||||
onBlur?.();
|
||||
}, [onBlur]);
|
||||
|
||||
@@ -114,13 +124,14 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
|
||||
(value: string) => {
|
||||
setCurrentValue(value);
|
||||
onChange?.(value);
|
||||
setHasChanged(true);
|
||||
},
|
||||
[onChange],
|
||||
[onChange, setHasChanged],
|
||||
);
|
||||
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Submit nearest form on Enter key press
|
||||
// Submit the nearest form on Enter key press
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key !== 'Enter') return;
|
||||
@@ -145,7 +156,7 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
|
||||
>
|
||||
<Label
|
||||
htmlFor={id.current}
|
||||
optional={!required}
|
||||
required={required}
|
||||
visuallyHidden={hideLabel}
|
||||
className={classNames(labelClassName)}
|
||||
>
|
||||
@@ -158,8 +169,9 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
|
||||
'x-theme-input',
|
||||
'relative w-full rounded-md text',
|
||||
'border',
|
||||
focused ? 'border-border-focus' : 'border-border',
|
||||
!isValid && '!border-danger',
|
||||
focused && !disabled ? 'border-border-focus' : 'border-border',
|
||||
disabled && 'border-dotted',
|
||||
!isValid && hasChanged && '!border-danger',
|
||||
size === 'md' && 'min-h-md',
|
||||
size === 'sm' && 'min-h-sm',
|
||||
size === 'xs' && 'min-h-xs',
|
||||
@@ -190,7 +202,12 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
|
||||
onChange={handleChange}
|
||||
onPaste={onPaste}
|
||||
onPasteOverwrite={onPasteOverwrite}
|
||||
className={classNames(editorClassName, multiLine && 'py-1.5')}
|
||||
disabled={disabled}
|
||||
className={classNames(
|
||||
editorClassName,
|
||||
multiLine && size === 'md' && 'py-1.5',
|
||||
multiLine && size === 'sm' && 'py-1',
|
||||
)}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
readOnly={readOnly}
|
||||
@@ -201,7 +218,7 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
|
||||
<IconButton
|
||||
title={obscured ? `Show ${label}` : `Obscure ${label}`}
|
||||
size="xs"
|
||||
className="mr-0.5 group/obscure !h-auto my-0.5"
|
||||
className={classNames("mr-0.5 group/obscure !h-auto my-0.5", disabled && 'opacity-disabled')}
|
||||
iconClassName="text-text-subtle group-hover/obscure:text"
|
||||
iconSize="sm"
|
||||
icon={obscured ? 'eye' : 'eye_closed'}
|
||||
|
||||
@@ -4,30 +4,32 @@ import type { HTMLAttributes } from 'react';
|
||||
export function Label({
|
||||
htmlFor,
|
||||
className,
|
||||
optional,
|
||||
children,
|
||||
visuallyHidden,
|
||||
otherTags = [],
|
||||
tags = [],
|
||||
required,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLLabelElement> & {
|
||||
htmlFor: string;
|
||||
optional?: boolean;
|
||||
otherTags?: string[];
|
||||
required?: boolean;
|
||||
tags?: string[];
|
||||
visuallyHidden?: boolean;
|
||||
}) {
|
||||
const tags = optional ? ['optional', ...otherTags] : otherTags;
|
||||
return (
|
||||
<label
|
||||
htmlFor={htmlFor}
|
||||
className={classNames(
|
||||
className,
|
||||
visuallyHidden && 'sr-only',
|
||||
'flex-shrink-0',
|
||||
'flex-shrink-0 text-sm',
|
||||
'text-text-subtle whitespace-nowrap flex items-center gap-1',
|
||||
)}
|
||||
htmlFor={htmlFor}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<span>
|
||||
{children}
|
||||
{required === true && <span className="text-text-subtlest">*</span>}
|
||||
</span>
|
||||
{tags.map((tag, i) => (
|
||||
<span key={i} className="text-xs text-text-subtlest">
|
||||
({tag})
|
||||
|
||||
@@ -168,7 +168,8 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(pair: PairWithId) => setPairsAndSave((pairs) => pairs.map((p) => (pair.id !== p.id ? p : pair))),
|
||||
(pair: PairWithId) =>
|
||||
setPairsAndSave((pairs) => pairs.map((p) => (pair.id !== p.id ? p : pair))),
|
||||
[setPairsAndSave],
|
||||
);
|
||||
|
||||
@@ -344,7 +345,6 @@ function PairEditorRow({
|
||||
const deleteItems = useMemo(
|
||||
(): DropdownItem[] => [
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Delete',
|
||||
onSelect: handleDelete,
|
||||
color: 'danger',
|
||||
@@ -570,7 +570,6 @@ function FileActionsDropdown({
|
||||
const extraItems = useMemo<DropdownItem[]>(
|
||||
() => [
|
||||
{
|
||||
key: 'mime',
|
||||
label: 'Set Content-Type',
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
hidden: !pair.isFile,
|
||||
@@ -589,7 +588,6 @@ function FileActionsDropdown({
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'clear-file',
|
||||
label: 'Unset File',
|
||||
leftSlot: <Icon icon="x" />,
|
||||
hidden: pair.isFile,
|
||||
@@ -598,7 +596,6 @@ function FileActionsDropdown({
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Delete',
|
||||
onSelect: onDelete,
|
||||
variant: 'danger',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes, FocusEvent } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import type { FocusEvent, HTMLAttributes } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||
import { IconButton } from './IconButton';
|
||||
import type { InputProps } from './Input';
|
||||
@@ -41,8 +41,8 @@ export function PlainInput({
|
||||
onFocusRaw,
|
||||
}: PlainInputProps) {
|
||||
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
|
||||
const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [hasChanged, setHasChanged] = useState<boolean>(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
@@ -71,19 +71,19 @@ export function PlainInput({
|
||||
'px-2 text-xs font-mono cursor-text',
|
||||
);
|
||||
|
||||
const isValid = useMemo(() => {
|
||||
if (required && !validateRequire(currentValue)) return false;
|
||||
if (typeof validate === 'boolean') return validate;
|
||||
if (typeof validate === 'function' && !validate(currentValue)) return false;
|
||||
return true;
|
||||
}, [required, currentValue, validate]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(value: string) => {
|
||||
setCurrentValue(value);
|
||||
onChange?.(value);
|
||||
setHasChanged(true);
|
||||
const isValid = (value: string) => {
|
||||
if (required && !validateRequire(value)) return false;
|
||||
if (typeof validate === 'boolean') return validate;
|
||||
if (typeof validate === 'function' && !validate(value)) return false;
|
||||
return true;
|
||||
};
|
||||
inputRef.current?.setCustomValidity(isValid(value) ? '' : 'Invalid value');
|
||||
},
|
||||
[onChange],
|
||||
[onChange, required, validate],
|
||||
);
|
||||
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
@@ -98,12 +98,7 @@ export function PlainInput({
|
||||
labelPosition === 'top' && 'flex-row gap-0.5',
|
||||
)}
|
||||
>
|
||||
<Label
|
||||
htmlFor={id}
|
||||
className={labelClassName}
|
||||
visuallyHidden={hideLabel}
|
||||
optional={!required}
|
||||
>
|
||||
<Label htmlFor={id} className={labelClassName} visuallyHidden={hideLabel} required={required}>
|
||||
{label}
|
||||
</Label>
|
||||
<HStack
|
||||
@@ -114,7 +109,7 @@ export function PlainInput({
|
||||
'relative w-full rounded-md text',
|
||||
'border',
|
||||
focused ? 'border-border-focus' : 'border-border-subtle',
|
||||
!isValid && '!border-danger',
|
||||
hasChanged && 'has-[:invalid]:border-danger', // For built-in HTML validation
|
||||
size === 'md' && 'min-h-md',
|
||||
size === 'sm' && 'min-h-sm',
|
||||
size === 'xs' && 'min-h-xs',
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { PromptTextRequest } from '@yaakapp-internal/plugins';
|
||||
import type { FormEvent, ReactNode } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Button } from './Button';
|
||||
import { Input } from './Input';
|
||||
import { PlainInput } from './PlainInput';
|
||||
import { HStack } from './Stacks';
|
||||
|
||||
export type PromptProps = Omit<PromptTextRequest, 'id' | 'title' | 'description'> & {
|
||||
@@ -35,7 +35,7 @@ export function Prompt({
|
||||
className="grid grid-rows-[auto_auto] grid-cols-[minmax(0,1fr)] gap-4 mb-4"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Input
|
||||
<PlainInput
|
||||
hideLabel
|
||||
autoSelect
|
||||
required={required}
|
||||
@@ -43,7 +43,6 @@ export function Prompt({
|
||||
label={label}
|
||||
defaultValue={defaultValue}
|
||||
onChange={setValue}
|
||||
stateKey={null}
|
||||
/>
|
||||
<HStack space={2} justifyContent="end">
|
||||
<Button onClick={onCancel} variant="border" color="secondary">
|
||||
|
||||
@@ -23,12 +23,14 @@ export interface SelectProps<T extends string> {
|
||||
size?: ButtonProps['size'];
|
||||
className?: string;
|
||||
event?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function Select<T extends string>({
|
||||
labelPosition = 'top',
|
||||
name,
|
||||
labelClassName,
|
||||
disabled,
|
||||
hideLabel,
|
||||
label,
|
||||
value,
|
||||
@@ -72,7 +74,8 @@ export function Select<T extends string>({
|
||||
'w-full rounded-md text text-sm font-mono',
|
||||
'pl-2',
|
||||
'border',
|
||||
focused ? 'border-border-focus' : 'border-border',
|
||||
focused && !disabled ? 'border-border-focus' : 'border-border',
|
||||
disabled && 'border-dotted',
|
||||
isInvalidSelection && 'border-danger',
|
||||
size === 'xs' && 'h-xs',
|
||||
size === 'sm' && 'h-sm',
|
||||
@@ -86,7 +89,8 @@ export function Select<T extends string>({
|
||||
onChange={(e) => handleChange(e.target.value as T)}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => setFocused(false)}
|
||||
className={classNames('pr-7 w-full outline-none bg-transparent')}
|
||||
className={classNames('pr-7 w-full outline-none bg-transparent disabled:opacity-disabled')}
|
||||
disabled={disabled}
|
||||
>
|
||||
{isInvalidSelection && <option value={'__NONE__'}>-- Select an Option --</option>}
|
||||
{options.map((o) => {
|
||||
@@ -109,6 +113,7 @@ export function Select<T extends string>({
|
||||
variant="border"
|
||||
size={size}
|
||||
leftSlot={leftSlot}
|
||||
disabled={disabled}
|
||||
forDropdown
|
||||
>
|
||||
{options.find((o) => o.type !== 'separator' && o.value === value)?.label ?? '--'}
|
||||
|
||||
@@ -20,9 +20,8 @@ export interface ToastProps {
|
||||
color?: ShowToastRequest['color'];
|
||||
}
|
||||
|
||||
const ICONS: Record<NonNullable<ToastProps['color']>, IconProps['icon'] | null> = {
|
||||
const ICONS: Record<NonNullable<ToastProps['color'] | 'custom'>, IconProps['icon'] | null> = {
|
||||
custom: null,
|
||||
default: 'info',
|
||||
danger: 'alert_triangle',
|
||||
info: 'info',
|
||||
notice: 'alert_triangle',
|
||||
@@ -42,9 +41,8 @@ export function Toast({ children, open, onClose, timeout, action, icon, color }:
|
||||
{},
|
||||
[open],
|
||||
);
|
||||
color = color ?? 'default';
|
||||
|
||||
const toastIcon = icon ?? (color in ICONS && ICONS[color]);
|
||||
const toastIcon = icon ?? (color && color in ICONS && ICONS[color]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
|
||||
Reference in New Issue
Block a user