A bunch of changes, including moving prompt/confirm out of context

This commit is contained in:
Gregory Schier
2025-01-07 06:56:51 -08:00
parent 4776bbc753
commit 2f7b66fc92
41 changed files with 315 additions and 353 deletions

View File

@@ -5,7 +5,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useCommands } from '../hooks/useCommands';
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
import { useCreateGrpcRequest } from '../hooks/useCreateGrpcRequest';
import { useCreateHttpRequest } from '../hooks/useCreateHttpRequest';
@@ -27,6 +26,7 @@ import { useScrollIntoView } from '../hooks/useScrollIntoView';
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { createFolder } from '../lib/commands';
import { showDialog, toggleDialog } from '../lib/dialog';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { router } from '../lib/router';
@@ -69,7 +69,6 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
const [recentRequests] = useRecentRequests();
const openWorkspace = useOpenWorkspace();
const createHttpRequest = useCreateHttpRequest();
const { createFolder } = useCommands();
const activeCookieJar = useActiveCookieJar();
const createGrpcRequest = useCreateGrpcRequest();
const createEnvironment = useCreateEnvironment();
@@ -188,7 +187,6 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
activeRequest,
baseEnvironment,
createEnvironment,
createFolder,
createGrpcRequest,
createHttpRequest,
createWorkspace,

View File

@@ -1,12 +1,13 @@
import { useAtomValue } from 'jotai';
import { memo, useMemo } from 'react';
import { setActiveCookieJar, useActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
import { cookieJarsAtom } from '../hooks/useCookieJars';
import { useCreateCookieJar } from '../hooks/useCreateCookieJar';
import { useDeleteCookieJar } from '../hooks/useDeleteCookieJar';
import { usePrompt } from '../hooks/usePrompt';
import { useUpdateCookieJar } from '../hooks/useUpdateCookieJar';
import { showDialog } from '../lib/dialog';
import { jotaiStore } from '../lib/jotai';
import { showPrompt } from '../lib/prompt';
import {setWorkspaceSearchParams} from "../lib/setWorkspaceSearchParams";
import { CookieDialog } from './CookieDialog';
import { Dropdown, type DropdownItem } from './core/Dropdown';
import { Icon } from './core/Icon';
@@ -18,18 +19,19 @@ export const CookieDropdown = memo(function CookieDropdown() {
const updateCookieJar = useUpdateCookieJar(activeCookieJar?.id ?? null);
const deleteCookieJar = useDeleteCookieJar(activeCookieJar ?? null);
const createCookieJar = useCreateCookieJar();
const prompt = usePrompt();
const cookieJars = useAtomValue(cookieJarsAtom);
const items = useMemo((): DropdownItem[] => {
const cookieJars = jotaiStore.get(cookieJarsAtom) ?? [];
return [
...cookieJars.map((j) => ({
...(cookieJars ?? []).map((j) => ({
key: j.id,
label: j.name,
leftSlot: <Icon icon={j.id === activeCookieJar?.id ? 'check' : 'empty'} />,
onSelect: () => setActiveCookieJar(j),
onSelect: () => {
setWorkspaceSearchParams({ cookie_jar_id: j.id });
},
})),
...((cookieJars.length > 0 && activeCookieJar != null
...(((cookieJars ?? []).length > 0 && activeCookieJar != null
? [
{ type: 'separator', label: activeCookieJar.name },
{
@@ -51,7 +53,7 @@ export const CookieDropdown = memo(function CookieDropdown() {
label: 'Rename',
leftSlot: <Icon icon="pencil" />,
onSelect: async () => {
const name = await prompt({
const name = await showPrompt({
id: 'rename-cookie-jar',
title: 'Rename Cookie Jar',
description: (
@@ -68,7 +70,7 @@ export const CookieDropdown = memo(function CookieDropdown() {
updateCookieJar.mutate({ name });
},
},
...((cookieJars.length > 1 // Never delete the last one
...(((cookieJars ?? []).length > 1 // Never delete the last one
? [
{
key: 'delete',
@@ -89,7 +91,7 @@ export const CookieDropdown = memo(function CookieDropdown() {
onSelect: () => createCookieJar.mutate(),
},
];
}, [activeCookieJar, createCookieJar, deleteCookieJar, prompt, updateCookieJar]);
}, [activeCookieJar, cookieJars, createCookieJar, deleteCookieJar, updateCookieJar]);
return (
<Dropdown items={items}>

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { useCommands } from '../hooks/useCommands';
import {createWorkspace} from "../lib/commands";
import { Button } from './core/Button';
import { PlainInput } from './core/PlainInput';
import { VStack } from './core/Stacks';
@@ -14,7 +14,6 @@ export function CreateWorkspaceDialog({ hide }: Props) {
const [name, setName] = useState<string>('');
const [description, setDescription] = useState<string>('');
const [settingSyncDir, setSettingSyncDir] = useState<string | null>(null);
const { createWorkspace } = useCommands();
return (
<VStack

View File

@@ -12,19 +12,26 @@ export function Dialogs() {
const dialogs = useAtomValue(dialogsAtom);
return (
<>
{dialogs.map(({ render, onClose, id, ...props }: DialogInstance) => (
<Dialog
open
key={id}
onClose={() => {
onClose?.();
hideDialog(id);
}}
{...props}
>
{render({ hide: () => hideDialog(id) })}
</Dialog>
{dialogs.map(({ id, ...props }) => (
<DialogInstance key={id} id={id} {...props} />
))}
</>
);
}
function DialogInstance({ render, onClose, id, ...props }: DialogInstance) {
const children = render({ hide: () => hideDialog(id) });
return (
<Dialog
open
key={id}
onClose={() => {
onClose?.();
hideDialog(id);
}}
{...props}
>
{children}
</Dialog>
);
}

View File

@@ -6,8 +6,8 @@ import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
import { useDeleteEnvironment } from '../hooks/useDeleteEnvironment';
import { useEnvironments } from '../hooks/useEnvironments';
import { useKeyValue } from '../hooks/useKeyValue';
import { usePrompt } from '../hooks/usePrompt';
import { useUpdateEnvironment } from '../hooks/useUpdateEnvironment';
import { showPrompt } from '../lib/prompt';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { ContextMenu } from './core/Dropdown';
@@ -212,7 +212,6 @@ function SidebarButton({
rightSlot?: ReactNode;
environment: Environment | null;
}) {
const prompt = usePrompt();
const updateEnvironment = useUpdateEnvironment(environment?.id ?? null);
const deleteEnvironment = useDeleteEnvironment(environment);
const [showContextMenu, setShowContextMenu] = useState<{
@@ -260,7 +259,7 @@ function SidebarButton({
label: 'Rename',
leftSlot: <Icon icon="pencil" size="sm" />,
onSelect: async () => {
const name = await prompt({
const name = await showPrompt({
id: 'rename-environment',
title: 'Rename Environment',
description: (

View File

@@ -2,34 +2,18 @@ import { emit } from '@tauri-apps/api/event';
import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugins';
import { useWatchWorkspace } from '@yaakapp-internal/sync';
import type { ShowToastRequest } from '@yaakapp/api';
import {
useEnsureActiveCookieJar,
useSubscribeActiveCookieJarId,
} from '../hooks/useActiveCookieJar';
import { useSubscribeActiveEnvironmentId } from '../hooks/useActiveEnvironment';
import { getActiveRequest, useActiveRequest } from '../hooks/useActiveRequest';
import { useSubscribeActiveRequestId } from '../hooks/useActiveRequestId';
import { useActiveWorkspace, useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace';
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest';
import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest';
import { useGenerateThemeCss } from '../hooks/useGenerateThemeCss';
import { useHotKey } from '../hooks/useHotKey';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useNotificationToast } from '../hooks/useNotificationToast';
import { usePrompt } from '../hooks/usePrompt';
import { useSubscribeRecentCookieJars } from '../hooks/useRecentCookieJars';
import { useSubscribeRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useSubscribeRecentRequests } from '../hooks/useRecentRequests';
import { useSubscribeRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting';
import { useSyncModelStores } from '../hooks/useSyncModelStores';
import { useSyncWorkspace } from '../hooks/useSyncWorkspace';
import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels';
import { useSyncWorkspaceRequestTitle } from '../hooks/useSyncWorkspaceRequestTitle';
import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting';
import { useSubscribeTemplateFunctions } from '../hooks/useTemplateFunctions';
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';
import { showPrompt } from '../lib/prompt';
import { showToast } from '../lib/toast';
export function GlobalHooks() {
@@ -37,17 +21,8 @@ export function GlobalHooks() {
useSyncZoomSetting();
useSyncFontSizeSetting();
useGenerateThemeCss();
useSyncWorkspaceRequestTitle();
useSubscribeActiveWorkspaceId();
useSubscribeActiveRequestId();
useSubscribeActiveEnvironmentId();
useSubscribeActiveCookieJarId();
useSubscribeRecentRequests();
useSubscribeRecentWorkspaces();
useSubscribeRecentEnvironments();
useSubscribeRecentCookieJars();
useSyncWorkspaceChildModels();
useSubscribeTemplateFunctions();
@@ -55,7 +30,6 @@ export function GlobalHooks() {
// Other useful things
useNotificationToast();
useActiveWorkspaceChangedToast();
useEnsureActiveCookieJar();
// Listen for toasts
useListenToTauriEvent<ShowToastRequest>('show_toast', (event) => {
@@ -68,32 +42,10 @@ export function GlobalHooks() {
useListenToTauriEvent('upserted_model', debouncedSync);
useWatchWorkspace(activeWorkspace, debouncedSync);
const activeRequest = useActiveRequest();
const duplicateHttpRequest = useDuplicateHttpRequest({
id: activeRequest?.id ?? null,
navigateAfter: true,
});
const duplicateGrpcRequest = useDuplicateGrpcRequest({
id: activeRequest?.id ?? null,
navigateAfter: true,
});
useHotKey('http_request.duplicate', async () => {
const activeRequest = getActiveRequest();
if (activeRequest?.model === 'http_request') {
await duplicateHttpRequest.mutateAsync();
} else {
await duplicateGrpcRequest.mutateAsync();
}
});
const toggleCommandPalette = useToggleCommandPalette();
useHotKey('command_palette.toggle', toggleCommandPalette);
const prompt = usePrompt();
useListenToTauriEvent<{ replyId: string; args: PromptTextRequest }>(
'show_prompt',
async (event) => {
const value = await prompt(event.payload.args);
const value = await showPrompt(event.payload.args);
const result: PromptTextResponse = { value };
await emit(event.payload.replyId, result);
},

View File

@@ -42,7 +42,7 @@ export function MarkdownEditor({ className, defaultValue, onChange, name, ...edi
<Editor
hideGutter
wrapLines
className="max-w-2xl max-h-full"
className="max-w-2xl max-h-full"
language="markdown"
defaultValue={defaultValue}
onChange={onChange}

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames';
import { memo, useMemo } from 'react';
import { usePrompt } from '../hooks/usePrompt';
import { showPrompt } from '../lib/prompt';
import { Button } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
import { Icon } from './core/Icon';
@@ -32,7 +32,6 @@ export const RequestMethodDropdown = memo(function RequestMethodDropdown({
onChange,
className,
}: Props) {
const prompt = usePrompt();
const extraItems = useMemo<DropdownItem[]>(
() => [
{
@@ -40,7 +39,7 @@ export const RequestMethodDropdown = memo(function RequestMethodDropdown({
label: 'CUSTOM',
leftSlot: <Icon icon="sparkles" />,
onSelect: async () => {
const newMethod = await prompt({
const newMethod = await showPrompt({
id: 'custom-method',
label: 'Http Method',
defaultValue: '',
@@ -54,7 +53,7 @@ export const RequestMethodDropdown = memo(function RequestMethodDropdown({
},
},
],
[onChange, prompt],
[onChange],
);
return (

View File

@@ -298,7 +298,6 @@ export const RequestPane = memo(function RequestPane({
);
const activeTab = activeTabs?.[activeRequestId];
console.log('ACTIVE TAB', activeTab);
const setActiveTab = useCallback(
(tab: string) => {
setActiveTabs((r) => ({ ...r, [activeRequest.id]: tab }));

View File

@@ -15,6 +15,7 @@ import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { router } from '../lib/router';
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
import { ContextMenu } from './core/Dropdown';
import { sidebarSelectedIdAtom, sidebarTreeAtom } from './SidebarAtoms';
import type { SidebarItemProps } from './SidebarItem';
@@ -151,11 +152,7 @@ export function Sidebar({ className }: Props) {
}
e.preventDefault();
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: activeWorkspace?.id ?? null },
search: (prev) => ({ ...prev, request_id: selected.id }),
});
setWorkspaceSearchParams({ request_id: selected.id });
});
useKey(

View File

@@ -2,13 +2,25 @@ import classNames from 'classnames';
import { motion } from 'framer-motion';
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest';
import {useEnsureActiveCookieJar, useSubscribeActiveCookieJarId} from "../hooks/useActiveCookieJar";
import {useSubscribeActiveEnvironmentId} from "../hooks/useActiveEnvironment";
import {getActiveRequest, useActiveRequest} from '../hooks/useActiveRequest';
import {useSubscribeActiveRequestId} from "../hooks/useActiveRequestId";
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import {useDuplicateGrpcRequest} from "../hooks/useDuplicateGrpcRequest";
import {useDuplicateHttpRequest} from "../hooks/useDuplicateHttpRequest";
import { useFloatingSidebarHidden } from '../hooks/useFloatingSidebarHidden';
import {useHotKey} from "../hooks/useHotKey";
import { useImportData } from '../hooks/useImportData';
import {useSubscribeRecentCookieJars} from "../hooks/useRecentCookieJars";
import {useSubscribeRecentEnvironments} from "../hooks/useRecentEnvironments";
import {useSubscribeRecentRequests} from "../hooks/useRecentRequests";
import {useSubscribeRecentWorkspaces} from "../hooks/useRecentWorkspaces";
import { useShouldFloatSidebar } from '../hooks/useShouldFloatSidebar';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useSidebarWidth } from '../hooks/useSidebarWidth';
import {useSyncWorkspaceRequestTitle} from "../hooks/useSyncWorkspaceRequestTitle";
import {useToggleCommandPalette} from "../hooks/useToggleCommandPalette";
import { useWorkspaces } from '../hooks/useWorkspaces';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
@@ -31,6 +43,9 @@ const body = { gridArea: 'body' };
const drag = { gridArea: 'drag' };
export function Workspace() {
// First, subscribe to some things applicable to workspaces
useGlobalWorkspaceHooks();
const workspaces = useWorkspaces();
const { setWidth, width, resetWidth } = useSidebarWidth();
const [sidebarHidden, setSidebarHidden] = useSidebarHidden();
@@ -202,3 +217,40 @@ function WorkspaceBody() {
return <HttpRequestLayout activeRequest={activeRequest} style={body} />;
}
function useGlobalWorkspaceHooks() {
useEnsureActiveCookieJar();
useSubscribeActiveRequestId();
useSubscribeActiveEnvironmentId();
useSubscribeActiveCookieJarId();
useSubscribeRecentRequests();
useSubscribeRecentWorkspaces();
useSubscribeRecentEnvironments();
useSubscribeRecentCookieJars();
useSyncWorkspaceRequestTitle();
const toggleCommandPalette = useToggleCommandPalette();
useHotKey('command_palette.toggle', toggleCommandPalette);
const activeRequest = useActiveRequest();
const duplicateHttpRequest = useDuplicateHttpRequest({
id: activeRequest?.id ?? null,
navigateAfter: true,
});
const duplicateGrpcRequest = useDuplicateGrpcRequest({
id: activeRequest?.id ?? null,
navigateAfter: true,
});
useHotKey('http_request.duplicate', async () => {
const activeRequest = getActiveRequest();
if (activeRequest?.model === 'http_request') {
await duplicateHttpRequest.mutateAsync();
} else {
await duplicateGrpcRequest.mutateAsync();
}
});
}

View File

@@ -5,7 +5,6 @@ import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDeleteSendHistory } from '../hooks/useDeleteSendHistory';
import { useOpenWorkspace } from '../hooks/useOpenWorkspace';
import { useSettings } from '../hooks/useSettings';
import { useSyncWorkspace } from '../hooks/useSyncWorkspace';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { showDialog } from '../lib/dialog';
import { getWorkspace } from '../lib/store';
@@ -31,7 +30,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
const settings = useSettings();
const openWorkspace = useOpenWorkspace();
const openWorkspaceNewWindow = settings?.openWorkspaceNewWindow ?? null;
const { sync } = useSyncWorkspace(activeWorkspace);
const orderedWorkspaces = useMemo(
() => [...workspaces].sort((a, b) => (a.name.localeCompare(b.name) > 0 ? 1 : -1)),
@@ -66,13 +64,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
});
},
},
{
key: 'sync',
label: 'Sync Workspace',
leftSlot: <Icon icon="folder_sync" />,
hidden: !activeWorkspace?.settingSyncDir,
onSelect: sync,
},
{
key: 'delete-responses',
label: 'Clear Send History',
@@ -89,14 +80,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
];
return { workspaceItems, extraItems };
}, [
orderedWorkspaces,
activeWorkspace?.settingSyncDir,
activeWorkspace?.id,
sync,
deleteSendHistory,
createWorkspace,
]);
}, [orderedWorkspaces, activeWorkspace?.id, deleteSendHistory, createWorkspace]);
const handleChange = useCallback(
async (workspaceId: string | null) => {

View File

@@ -0,0 +1,21 @@
import type { ReactNode } from 'react';
import { Button } from './Button';
import { HStack, VStack } from './Stacks';
export interface AlertProps {
onHide: () => void;
body: ReactNode;
}
export function Alert({ onHide, body }: AlertProps) {
return (
<VStack space={3} className="pb-4">
<div>{body}</div>
<HStack space={2} justifyContent="end">
<Button className="focus" color="primary" onClick={onHide}>
Okay
</Button>
</HStack>
</VStack>
);
}

View File

@@ -0,0 +1,43 @@
import type { ButtonProps } from './Button';
import { Button } from './Button';
import { HStack } from './Stacks';
export interface ConfirmProps {
onHide: () => void;
onResult: (result: boolean) => void;
variant?: 'delete' | 'confirm';
confirmText?: string;
}
const colors: Record<NonNullable<ConfirmProps['variant']>, ButtonProps['color']> = {
delete: 'danger',
confirm: 'primary',
};
const confirmButtonTexts: Record<NonNullable<ConfirmProps['variant']>, string> = {
delete: 'Delete',
confirm: 'Confirm',
};
export function Confirm({ onHide, onResult, confirmText, variant = 'confirm' }: ConfirmProps) {
const handleHide = () => {
onResult(false);
onHide();
};
const handleSuccess = () => {
onResult(true);
onHide();
};
return (
<HStack space={2} justifyContent="start" className="mt-2 mb-4 flex-row-reverse">
<Button color={colors[variant]} onClick={handleSuccess}>
{confirmText ?? confirmButtonTexts[variant]}
</Button>
<Button onClick={handleHide} variant="border">
Cancel
</Button>
</HStack>
);
}

View File

@@ -12,9 +12,9 @@ import {
} from 'react';
import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd';
import { usePrompt } from '../../hooks/usePrompt';
import { useToggle } from '../../hooks/useToggle';
import { generateId } from '../../lib/generateId';
import { showPrompt } from '../../lib/prompt';
import { DropMarker } from '../DropMarker';
import { SelectFile } from '../SelectFile';
import { Button } from './Button';
@@ -556,7 +556,6 @@ function FileActionsDropdown({
onChangeContentType: (contentType: string) => void;
onDelete: () => void;
}) {
const prompt = usePrompt();
const onChange = useCallback(
(v: string) => {
if (v === 'file') onChangeFile({ filePath: '' });
@@ -573,7 +572,7 @@ function FileActionsDropdown({
leftSlot: <Icon icon="pencil" />,
hidden: !pair.isFile,
onSelect: async () => {
const contentType = await prompt({
const contentType = await showPrompt({
id: 'content-type',
require: false,
title: 'Override Content-Type',
@@ -604,7 +603,7 @@ function FileActionsDropdown({
leftSlot: <Icon icon="trash" />,
},
],
[onChangeContentType, onChangeFile, onDelete, pair.contentType, pair.isFile, prompt],
[onChangeContentType, onChangeFile, onDelete, pair.contentType, pair.isFile],
);
return (

View File

@@ -0,0 +1,57 @@
import type { PromptTextRequest } from '@yaakapp-internal/plugins';
import type { FormEvent, ReactNode } from 'react';
import { useCallback, useState } from 'react';
import {PlainInput} from "./PlainInput";
import { HStack } from './Stacks';
import { Button } from './Button';
export type PromptProps = Omit<PromptTextRequest, 'id' | 'title' | 'description'> & {
description?: ReactNode;
onCancel: () => void;
onResult: (value: string | null) => void;
};
export function Prompt({
onCancel,
label,
defaultValue,
placeholder,
onResult,
require,
confirmText,
cancelText,
}: PromptProps) {
const [value, setValue] = useState<string>(defaultValue ?? '');
const handleSubmit = useCallback(
(e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
onResult(value);
},
[onResult, value],
);
return (
<form
className="grid grid-rows-[auto_auto] grid-cols-[minmax(0,1fr)] gap-4 mb-4"
onSubmit={handleSubmit}
>
<PlainInput
hideLabel
autoSelect
require={require}
placeholder={placeholder ?? 'Enter text'}
label={label}
defaultValue={defaultValue}
onChange={setValue}
/>
<HStack space={2} justifyContent="end">
<Button onClick={onCancel} variant="border" color="secondary">
{cancelText || 'Cancel'}
</Button>
<Button type="submit" color="primary">
{confirmText || 'Done'}
</Button>
</HStack>
</form>
);
}