Skip to content

Commit d354c00

Browse files
authored
feat: allow user to create groups (#783)
* add use-prompt hook * add create group modal * allow adding groups through tghe ui * Fix TypeScript linting errors in group creation tests Replace explicit 'any' types with proper type annotations in the mock mutation return value. Use 'as const' for status literal type and explicitly declare mock function variables to avoid TypeScript errors. * Refactor usePrompt to support Formik-style forms with react-hook-form and Zod Add new usePromptForm hook that supports complex forms with: - React Hook Form integration for form state management - Zod schema validation for type-safe form data - Custom form field rendering with full control over UI - Backward compatibility with existing usePrompt usage Key changes: - Add FormPromptConfig type for form-based prompts - Create FormPromptDialog component for form rendering - Update PromptProvider to handle both legacy and form prompts - Add usePromptForm hook alongside existing usePrompt - Update group creation to use new form-based approach - Add comprehensive tests and documentation examples The legacy usePrompt remains unchanged for simple input prompts, while usePromptForm provides a powerful alternative for complex multi-field forms with advanced validation. * simplify * proper error handling * use formik * . * fixes * fixes * more fixes * cleanup * cleanup * . * . * cleanpu * cleanpu * add jsdoc * cleanup * cleanup * layout fixes * fix validation * migrate to react hook form * cleanpu * improve validation logic * auto format * cleanup * cleanpu * refactor * simplify types * make group names case sensitive * cleanpu * change confusing naming * remove hacky stuff from test * disable usePrompt submit button when form is invalid
1 parent 1313d09 commit d354c00

File tree

12 files changed

+1061
-35
lines changed

12 files changed

+1061
-35
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import {
2+
Dialog,
3+
DialogContent,
4+
DialogHeader,
5+
DialogFooter,
6+
DialogTitle,
7+
DialogDescription,
8+
} from '@/common/components/ui/dialog'
9+
import { Button } from '@/common/components/ui/button'
10+
import type { ReactHookFormPromptConfig } from '.'
11+
import { useForm } from 'react-hook-form'
12+
import type { UseFormReturn } from 'react-hook-form'
13+
14+
interface FormDialogProps {
15+
isOpen: boolean
16+
config: ReactHookFormPromptConfig<Record<string, unknown>>
17+
onSubmit: (data: Record<string, unknown>) => void
18+
onCancel: () => void
19+
onOpenChange: (open: boolean) => void
20+
}
21+
22+
export function FormDialog({
23+
isOpen,
24+
config,
25+
onSubmit,
26+
onCancel,
27+
onOpenChange,
28+
}: FormDialogProps) {
29+
const form = useForm({
30+
defaultValues: config.defaultValues,
31+
resolver: config.resolver,
32+
mode: 'onChange',
33+
})
34+
35+
const handleOpenChange = (open: boolean) => {
36+
if (!open) {
37+
onCancel()
38+
} else {
39+
onOpenChange(open)
40+
}
41+
}
42+
43+
const handleSubmit = (data: Record<string, unknown>) => {
44+
onSubmit(data)
45+
}
46+
47+
const handleCancel = () => {
48+
form.reset()
49+
onCancel()
50+
}
51+
52+
return (
53+
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
54+
<DialogContent>
55+
<form onSubmit={form.handleSubmit(handleSubmit)}>
56+
<DialogHeader>
57+
<DialogTitle>{config.title || 'Form Input'}</DialogTitle>
58+
<DialogDescription>{config.description || ''}</DialogDescription>
59+
</DialogHeader>
60+
61+
<div className="space-y-4 py-4">
62+
{config.fields(form as UseFormReturn<Record<string, unknown>>)}
63+
</div>
64+
65+
<DialogFooter>
66+
<Button variant="outline" onClick={handleCancel} type="button">
67+
{config.buttons?.cancel ?? 'Cancel'}
68+
</Button>
69+
<Button
70+
type="submit"
71+
disabled={form.formState.isSubmitting || !form.formState.isValid}
72+
>
73+
{config.buttons?.confirm ?? 'OK'}
74+
</Button>
75+
</DialogFooter>
76+
</form>
77+
</DialogContent>
78+
</Dialog>
79+
)
80+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { createContext, type ReactNode } from 'react'
2+
import type { UseFormReturn, FieldValues, Resolver } from 'react-hook-form'
3+
4+
export type ReactHookFormPromptConfig<TValues extends FieldValues> = {
5+
title?: string
6+
description?: ReactNode
7+
defaultValues: TValues
8+
resolver?: Resolver<TValues>
9+
fields: (form: UseFormReturn<TValues>) => ReactNode
10+
buttons?: {
11+
confirm?: string
12+
cancel?: string
13+
}
14+
}
15+
16+
export type PromptContextType = {
17+
promptForm: <TValues extends Record<string, unknown>>(
18+
config: ReactHookFormPromptConfig<TValues>
19+
) => Promise<TValues | null>
20+
}
21+
22+
export const PromptContext = createContext<PromptContextType | null>(null)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { useState, type ReactNode } from 'react'
2+
import { PromptContext, type ReactHookFormPromptConfig } from '.'
3+
import { FormDialog } from './form-prompt-dialog'
4+
5+
export function PromptProvider({ children }: { children: ReactNode }) {
6+
const [activePrompt, setActivePrompt] = useState<{
7+
config: ReactHookFormPromptConfig<Record<string, unknown>>
8+
resolve: (value: unknown) => void
9+
} | null>(null)
10+
11+
const [isOpen, setIsOpen] = useState(false)
12+
13+
const promptForm = <TValues extends Record<string, unknown>>(
14+
config: ReactHookFormPromptConfig<TValues>
15+
) => {
16+
return new Promise<TValues | null>((resolve) => {
17+
setActivePrompt({
18+
config: config as ReactHookFormPromptConfig<Record<string, unknown>>,
19+
resolve: (value: unknown) => {
20+
resolve(value as TValues)
21+
},
22+
})
23+
setIsOpen(true)
24+
})
25+
}
26+
27+
const handleSubmit = (data: unknown) => {
28+
if (!activePrompt) return
29+
activePrompt.resolve(data)
30+
closeDialog()
31+
}
32+
33+
const handleCancel = () => {
34+
if (activePrompt) {
35+
activePrompt.resolve(null)
36+
}
37+
closeDialog()
38+
}
39+
40+
const closeDialog = () => {
41+
setIsOpen(false)
42+
setActivePrompt(null)
43+
}
44+
45+
const handleOpenChange = (open: boolean) => {
46+
if (!open) {
47+
handleCancel()
48+
}
49+
}
50+
51+
return (
52+
<PromptContext.Provider value={{ promptForm }}>
53+
{children}
54+
55+
{activePrompt && (
56+
<FormDialog
57+
isOpen={isOpen}
58+
config={activePrompt.config}
59+
onSubmit={handleSubmit as (v: Record<string, unknown>) => void}
60+
onCancel={handleCancel}
61+
onOpenChange={handleOpenChange}
62+
/>
63+
)}
64+
</PromptContext.Provider>
65+
)
66+
}

0 commit comments

Comments
 (0)