Pair validation

This commit is contained in:
Gregory Schier
2023-03-20 00:17:29 -07:00
parent b16d74d55b
commit 333b9319b6
4 changed files with 60 additions and 57 deletions

View File

@@ -1,6 +1,6 @@
import classnames from 'classnames'; import classnames from 'classnames';
import type { FormEvent } from 'react'; import type { FormEvent } from 'react';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback } from 'react';
import { useIsResponseLoading } from '../hooks/useIsResponseLoading'; import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
import { useSendRequest } from '../hooks/useSendRequest'; import { useSendRequest } from '../hooks/useSendRequest';
import { useUpdateRequest } from '../hooks/useUpdateRequest'; import { useUpdateRequest } from '../hooks/useUpdateRequest';
@@ -20,7 +20,6 @@ export const UrlBar = memo(function UrlBar({ request, className }: Props) {
const handleMethodChange = useCallback((method: string) => updateRequest.mutate({ method }), []); const handleMethodChange = useCallback((method: string) => updateRequest.mutate({ method }), []);
const handleUrlChange = useCallback((url: string) => updateRequest.mutate({ url }), []); const handleUrlChange = useCallback((url: string) => updateRequest.mutate({ url }), []);
const loading = useIsResponseLoading(request.id); const loading = useIsResponseLoading(request.id);
const useEditor = useMemo(() => ({ useTemplating: true, contentType: 'url' }), []);
const handleSubmit = useCallback( const handleSubmit = useCallback(
async (e: FormEvent) => { async (e: FormEvent) => {
@@ -35,7 +34,8 @@ export const UrlBar = memo(function UrlBar({ request, className }: Props) {
<Input <Input
key={request.id} key={request.id}
hideLabel hideLabel
useEditor={useEditor} useTemplating
contentType="url"
className="px-0" className="px-0"
name="url" name="url"
label="Enter URL" label="Enter URL"

View File

@@ -1,26 +1,29 @@
import classnames from 'classnames'; import classnames from 'classnames';
import { useMemo, useState } from 'react';
import type { HTMLAttributes, ReactNode } from 'react'; import type { HTMLAttributes, ReactNode } from 'react';
import type { EditorProps } from './Editor'; import type { EditorProps } from './Editor';
import { Editor } from './Editor'; import { Editor } from './Editor';
import { HStack, VStack } from './Stacks'; import { HStack, VStack } from './Stacks';
type Props = Omit<HTMLAttributes<HTMLInputElement>, 'onChange' | 'onFocus'> & { type Props = Omit<HTMLAttributes<HTMLInputElement>, 'onChange' | 'onFocus'> &
name: string; Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete'> & {
label: string; name: string;
hideLabel?: boolean; label: string;
labelClassName?: string; hideLabel?: boolean;
containerClassName?: string; labelClassName?: string;
onChange?: (value: string) => void; containerClassName?: string;
onFocus?: () => void; onChange?: (value: string) => void;
useEditor?: Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete'>; onFocus?: () => void;
defaultValue?: string; defaultValue?: string;
leftSlot?: ReactNode; leftSlot?: ReactNode;
rightSlot?: ReactNode; rightSlot?: ReactNode;
size?: 'sm' | 'md'; size?: 'sm' | 'md';
className?: string; className?: string;
placeholder?: string; placeholder?: string;
autoFocus?: boolean; autoFocus?: boolean;
}; validate?: (v: string) => boolean;
require?: boolean;
};
export function Input({ export function Input({
label, label,
@@ -31,13 +34,15 @@ export function Input({
onChange, onChange,
placeholder, placeholder,
size = 'md', size = 'md',
useEditor,
name, name,
leftSlot, leftSlot,
rightSlot, rightSlot,
defaultValue, defaultValue,
validate,
require,
...props ...props
}: Props) { }: Props) {
const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
const id = `input-${name}`; const id = `input-${name}`;
const inputClassName = classnames( const inputClassName = classnames(
className, className,
@@ -46,6 +51,17 @@ export function Input({
!!rightSlot && '!pr-0.5', !!rightSlot && '!pr-0.5',
); );
const isValid = useMemo(() => {
if (require && !validateRequire(currentValue)) return false;
if (validate && !validate(currentValue)) return false;
return true;
}, [currentValue, validate, require]);
const handleChange = (value: string) => {
setCurrentValue(value);
onChange?.(value);
};
return ( return (
<VStack> <VStack>
<label <label
@@ -64,34 +80,27 @@ export function Input({
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-focus', 'border border-gray-200 focus-within:border-focus',
!isValid && 'border-invalid',
size === 'md' && 'h-9', size === 'md' && 'h-9',
size === 'sm' && 'h-7', size === 'sm' && 'h-7',
)} )}
> >
{leftSlot} {leftSlot}
{useEditor ? ( <Editor
<Editor id={id}
id={id} singleLine
singleLine defaultValue={defaultValue}
defaultValue={defaultValue} placeholder={placeholder}
placeholder={placeholder} onChange={handleChange}
onChange={onChange} className={inputClassName}
className={inputClassName} {...props}
{...props} />
{...useEditor}
/>
) : (
<input
id={id}
onChange={(e) => onChange?.(e.currentTarget.value)}
placeholder={placeholder}
defaultValue={defaultValue}
className={inputClassName}
{...props}
/>
)}
{rightSlot} {rightSlot}
</HStack> </HStack>
</VStack> </VStack>
); );
} }
function validateRequire(v: string) {
return v.length > 0;
}

View File

@@ -200,16 +200,6 @@ const FormRow = memo(function FormRow({
[onChange, pairContainer.pair.name, pairContainer.pair.enabled], [onChange, pairContainer.pair.name, pairContainer.pair.enabled],
); );
const nameEditorConfig = useMemo(
() => ({ useTemplating: true, autocomplete: nameAutocomplete }),
[nameAutocomplete],
);
const valueEditorConfig = useMemo(
() => ({ useTemplating: true, autocomplete: valueAutocomplete?.(pairContainer.pair.name) }),
[valueAutocomplete, pairContainer.pair.name],
);
const handleFocus = useCallback(() => onFocus?.(pairContainer), [onFocus, pairContainer]); const handleFocus = useCallback(() => onFocus?.(pairContainer), [onFocus, pairContainer]);
const handleDelete = useCallback(() => onDelete?.(pairContainer), [onDelete, pairContainer]); const handleDelete = useCallback(() => onDelete?.(pairContainer), [onDelete, pairContainer]);
@@ -263,13 +253,15 @@ const FormRow = memo(function FormRow({
<span className="w-1" /> <span className="w-1" />
)} )}
<Checkbox <Checkbox
disabled={isLast} disabled={isLast || !pairContainer.pair.name}
checked={!!pairContainer.pair.enabled} checked={isLast || !pairContainer.pair.name ? false : !!pairContainer.pair.enabled}
onChange={handleChangeEnabled}
className={isLast ? '!opacity-disabled' : undefined} className={isLast ? '!opacity-disabled' : undefined}
onChange={handleChangeEnabled}
/> />
<Input <Input
hideLabel hideLabel
require={!isLast && !!pairContainer.pair.enabled && !!pairContainer.pair.value}
useTemplating
containerClassName={classnames(isLast && 'border-dashed')} containerClassName={classnames(isLast && 'border-dashed')}
defaultValue={pairContainer.pair.name} defaultValue={pairContainer.pair.name}
label="Name" label="Name"
@@ -277,7 +269,7 @@ const FormRow = memo(function FormRow({
onChange={handleChangeName} onChange={handleChangeName}
onFocus={handleFocus} onFocus={handleFocus}
placeholder={namePlaceholder ?? 'name'} placeholder={namePlaceholder ?? 'name'}
useEditor={nameEditorConfig} autocomplete={nameAutocomplete}
/> />
<Input <Input
hideLabel hideLabel
@@ -288,7 +280,8 @@ const FormRow = memo(function FormRow({
onChange={handleChangeValue} onChange={handleChangeValue}
onFocus={handleFocus} onFocus={handleFocus}
placeholder={valuePlaceholder ?? 'value'} placeholder={valuePlaceholder ?? 'value'}
useEditor={valueEditorConfig} useTemplating
autocomplete={valueAutocomplete?.(pairContainer.pair.name)}
/> />
{onDelete ? ( {onDelete ? (
<IconButton <IconButton

View File

@@ -26,6 +26,7 @@ module.exports = {
}, },
colors: { colors: {
focus: "hsl(var(--color-blue-500) / 0.6)", focus: "hsl(var(--color-blue-500) / 0.6)",
invalid: "hsl(var(--color-red-500))",
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>)",