Skip to content

Commit 912dac2

Browse files
STNDP-32 Support keyboard shortcut for standup forms (#41)
1 parent b45b661 commit 912dac2

2 files changed

Lines changed: 102 additions & 25 deletions

File tree

apps/web/app/routes/board-route/dynamic-form.tsx

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
22
import { Flex, Box, TextArea, Button, Text, Skeleton } from "@radix-ui/themes";
33
import { useForm, Controller } from "react-hook-form";
44
import { z } from "zod";
5+
import { useImperativeHandle, type Ref } from "react";
56

67
export function validateDynamicFormSchema(schema: unknown) {
78
try {
@@ -62,13 +63,19 @@ export type DynamicFormValues = {
6263
[key: string]: any; // Allows dynamic field names
6364
};
6465

66+
export type DynamicFormRef = {
67+
submit: () => void;
68+
};
69+
6570
function DynamicForm({
71+
ref,
6672
schema,
6773
defaultValues,
6874
onSubmit,
6975
onCancel,
7076
loading,
7177
}: {
78+
ref: Ref<DynamicFormRef>;
7279
schema: z.infer<typeof dynamicFormSchema>;
7380
defaultValues?: DynamicFormValues;
7481
onSubmit: (data: DynamicFormValues) => void;
@@ -78,23 +85,26 @@ function DynamicForm({
7885
const { title, description, fields } = schema;
7986

8087
const dynamicFormSchema = z.object(
81-
fields.reduce((acc, field) => {
82-
if (field.type === "textarea") {
83-
let fieldValidation = z.string();
88+
fields.reduce(
89+
(acc, field) => {
90+
if (field.type === "textarea") {
91+
let fieldValidation = z.string();
8492

85-
if (field.validations) {
86-
fieldValidation = fieldValidation
87-
.min(field.validations.minLength || 0)
88-
.max(field.validations.maxLength || Infinity);
89-
}
93+
if (field.validations) {
94+
fieldValidation = fieldValidation
95+
.min(field.validations.minLength || 0)
96+
.max(field.validations.maxLength || Infinity);
97+
}
9098

91-
acc[field.name] = field.required
92-
? fieldValidation.nonempty()
93-
: fieldValidation.optional();
94-
}
99+
acc[field.name] = field.required
100+
? fieldValidation.nonempty()
101+
: fieldValidation.optional();
102+
}
95103

96-
return acc;
97-
}, {} as { [key: string]: z.ZodType })
104+
return acc;
105+
},
106+
{} as { [key: string]: z.ZodType }
107+
)
98108
);
99109

100110
const {
@@ -111,13 +121,22 @@ function DynamicForm({
111121
}, {} as DynamicFormValues),
112122
});
113123

124+
useImperativeHandle(
125+
ref,
126+
() => ({
127+
submit: () => {
128+
handleFormSubmit();
129+
},
130+
}),
131+
[]
132+
);
133+
134+
const handleFormSubmit = handleSubmit((data) => {
135+
onSubmit(data);
136+
});
137+
114138
return (
115-
<form
116-
method="post"
117-
onSubmit={handleSubmit((data) => {
118-
onSubmit(data);
119-
})}
120-
>
139+
<form method="post" onSubmit={handleFormSubmit}>
121140
<Flex direction="column">
122141
<Text size="4" weight="bold">
123142
{title}

apps/web/app/routes/board-route/todays-standup.tsx

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
import { Box, Button, Card, Flex, Text } from "@radix-ui/themes";
2-
import { Suspense, use, useEffect, useState } from "react";
2+
import {
3+
Suspense,
4+
useEffect,
5+
useState,
6+
use,
7+
useRef,
8+
useImperativeHandle,
9+
type Ref,
10+
} from "react";
311
import { useFetcher, useLoaderData } from "react-router";
412
import type { loader } from "./board-route";
513
import { DateTime } from "luxon";
614
import DynamicForm, {
715
FormSkeleton,
16+
type DynamicFormRef,
817
validateDynamicFormSchema,
918
type DynamicFormValues,
1019
} from "./dynamic-form";
@@ -20,7 +29,13 @@ import { parseMarkdownToHtml } from "~/libs/markdown";
2029

2130
type Props = {};
2231

23-
function CardContent() {
32+
interface CardContentRef {
33+
edit: () => void;
34+
cancel: () => void;
35+
save: () => void;
36+
}
37+
38+
function CardContent({ ref }: { ref: Ref<CardContentRef> }) {
2439
const {
2540
boardPromise,
2641
standupsPromise,
@@ -33,6 +48,24 @@ function CardContent() {
3348

3449
const schema = validateDynamicFormSchema(structure?.schema);
3550

51+
const dynamicFormRef = useRef<DynamicFormRef>(null);
52+
53+
useImperativeHandle(
54+
ref,
55+
() => ({
56+
edit: () => {
57+
handleEditButtonClick();
58+
},
59+
cancel: () => {
60+
handleDynamicFormCancel();
61+
},
62+
save: () => {
63+
dynamicFormRef.current?.submit();
64+
},
65+
}),
66+
[]
67+
);
68+
3669
if (!schema) {
3770
return null;
3871
}
@@ -111,10 +144,19 @@ function CardContent() {
111144

112145
const [isEditing, setIsEditing] = useState(!Boolean(todayStandup));
113146

147+
function handleDynamicFormCancel() {
148+
setIsEditing(false);
149+
}
150+
151+
function handleEditButtonClick() {
152+
setIsEditing(true);
153+
}
154+
114155
return (
115156
<>
116157
{!todayStandup && (
117158
<DynamicForm
159+
ref={dynamicFormRef}
118160
schema={schema}
119161
onSubmit={async (data) => {
120162
if (!structure) {
@@ -139,6 +181,7 @@ function CardContent() {
139181
{todayStandup &&
140182
(isEditing ? (
141183
<DynamicForm
184+
ref={dynamicFormRef}
142185
schema={schema}
143186
defaultValues={todayStandup.formData as DynamicFormValues}
144187
onSubmit={async (data) => {
@@ -154,7 +197,7 @@ function CardContent() {
154197
);
155198
setIsEditing(false);
156199
}}
157-
onCancel={() => setIsEditing(false)}
200+
onCancel={handleDynamicFormCancel}
158201
/>
159202
) : (
160203
<Flex direction="column" gap="5">
@@ -193,7 +236,7 @@ function CardContent() {
193236
highContrast
194237
size="2"
195238
variant="surface"
196-
onClick={() => setIsEditing(true)}
239+
onClick={handleEditButtonClick}
197240
>
198241
Edit
199242
</Button>
@@ -205,16 +248,31 @@ function CardContent() {
205248
}
206249

207250
function TodaysStandup({}: Props) {
251+
const cardContentRef = useRef<CardContentRef>(null);
252+
208253
return (
209254
<Card
210255
tabIndex={0}
211256
size={{
212257
initial: "2",
213258
sm: "4",
214259
}}
260+
onKeyDown={(event) => {
261+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
262+
cardContentRef.current?.save();
263+
}
264+
265+
if (event.key === "e") {
266+
cardContentRef.current?.edit();
267+
}
268+
269+
if (event.key === "Escape") {
270+
cardContentRef.current?.cancel();
271+
}
272+
}}
215273
>
216274
<Suspense fallback={<FormSkeleton />}>
217-
<CardContent />
275+
<CardContent ref={cardContentRef} />
218276
</Suspense>
219277
</Card>
220278
);

0 commit comments

Comments
 (0)