Skip to content

Commit 37cc43c

Browse files
authored
Merge pull request #27 from database-playground/pan93412/dbp-119-admin-可以格式化-sql-程式碼
DBP-119: admin can format SQL code
2 parents 49f6dda + 4d08336 commit 37cc43c

File tree

12 files changed

+1250
-913
lines changed

12 files changed

+1250
-913
lines changed

app/(admin)/(question-management)/database/_components/update-form.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import SQLEditor from "@/components/sql-editor";
12
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
23
import { Input } from "@/components/ui/input";
34
import { Textarea } from "@/components/ui/textarea";
@@ -78,10 +79,9 @@ export function UpdateDatabaseForm({
7879
<FormItem>
7980
<FormLabel>資料結構</FormLabel>
8081
<FormControl>
81-
<Textarea
82+
<SQLEditor
8283
{...field}
8384
placeholder="請輸入完整的 SQL 建表語句"
84-
className="max-h-[500px] min-h-[200px] font-mono text-sm"
8585
/>
8686
</FormControl>
8787
<FormDescription>

app/(admin)/(question-management)/questions/[id]/_components/header.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { Badge } from "@/components/ui/badge";
44
import { useSuspenseQuery } from "@apollo/client/react";
55
import { Suspense } from "react";
6+
import { Remark } from "react-remark";
67
import { QUESTION_DETAIL_QUERY } from "./query";
78

89
export function Header({ id }: { id: string }) {
@@ -37,7 +38,9 @@ function HeaderMain({ id }: { id: string }) {
3738
<Badge variant="outline">{question.category}</Badge>
3839
<Badge variant={difficultyInfo.variant}>{difficultyInfo.label}</Badge>
3940
</div>
40-
<p className="text-muted-foreground">{description}</p>
41+
<div className="text-muted-foreground">
42+
<Remark>{description}</Remark>
43+
</div>
4144
</div>
4245
);
4346
}

app/(admin)/(question-management)/questions/_components/create.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ import {
1010
DialogTitle,
1111
DialogTrigger,
1212
} from "@/components/ui/dialog";
13+
import { graphql } from "@/gql";
1314
import { QuestionDifficulty } from "@/gql/graphql";
1415
import { useDialogCloseConfirmation } from "@/hooks/use-dialog-close-confirmation";
1516
import { useMutation, useSuspenseQuery } from "@apollo/client/react";
1617
import { useRouter } from "next/navigation";
1718
import { useState } from "react";
1819
import { toast } from "sonner";
1920
import { QUESTION_CREATE_MUTATION } from "./mutation";
20-
import { DATABASE_LIST_QUERY, QUESTIONS_TABLE_QUERY } from "./query";
21+
import { QUESTIONS_TABLE_QUERY } from "./query";
2122
import { UpdateQuestionForm, type UpdateQuestionFormData } from "./update-form";
2223

2324
export function CreateQuestionTrigger() {
@@ -68,14 +69,20 @@ export function CreateQuestionTrigger() {
6869
);
6970
}
7071

72+
const CREATE_QUESTION_DIALOG_CONTENT_QUERY = graphql(`
73+
query CreateQuestionDialogContent {
74+
...QuestionUpdateForm
75+
}
76+
`);
77+
7178
function CreateQuestionDialogContent({
7279
onCompleted,
7380
onFormStateChange,
7481
}: {
7582
onCompleted: () => void;
7683
onFormStateChange: (isDirty: boolean) => void;
7784
}) {
78-
const { data: databaseList } = useSuspenseQuery(DATABASE_LIST_QUERY);
85+
const { data } = useSuspenseQuery(CREATE_QUESTION_DIALOG_CONTENT_QUERY);
7986

8087
const [createQuestion] = useMutation(QUESTION_CREATE_MUTATION, {
8188
refetchQueries: [{ query: QUESTIONS_TABLE_QUERY }],
@@ -138,7 +145,7 @@ function CreateQuestionDialogContent({
138145
onSubmit={onSubmit}
139146
action="create"
140147
onFormStateChange={onFormStateChange}
141-
databaseList={databaseList.databases}
148+
fragment={data}
142149
/>
143150
</DialogContent>
144151
);

app/(admin)/(question-management)/questions/_components/update-form.tsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1+
import SQLEditor from "@/components/sql-editor";
12
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
23
import { Input } from "@/components/ui/input";
34
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
45
import { Textarea } from "@/components/ui/textarea";
56
import { UpdateFormBody } from "@/components/update-modal/form-body";
67
import type { UpdateFormBaseProps } from "@/components/update-modal/types";
8+
import { type FragmentType, graphql, useFragment } from "@/gql";
79
import { QuestionDifficulty } from "@/gql/graphql";
810
import { zodResolver } from "@hookform/resolvers/zod";
9-
import React from "react";
1011
import { useForm } from "react-hook-form";
1112
import { z } from "zod";
1213

@@ -28,9 +29,20 @@ export interface UpdateQuestionFormData {
2829
databaseID?: string; // Changed to single databaseID for 1-N relationship
2930
}
3031

32+
const QUESTION_UPDATE_FORM_FRAGEMENT = graphql(`
33+
fragment QuestionUpdateForm on Query {
34+
databases {
35+
id
36+
slug
37+
}
38+
39+
questionCategories
40+
}
41+
`);
42+
3143
export interface UpdateQuestionFormProps extends Omit<UpdateFormBaseProps<z.infer<typeof formSchema>>, "onSubmit"> {
3244
onSubmit: (newValues: UpdateQuestionFormData) => void;
33-
databaseList: { id: string; slug: string; description?: string | null }[];
45+
fragment: FragmentType<typeof QUESTION_UPDATE_FORM_FRAGEMENT>;
3446
}
3547

3648
const difficultyOptions = [
@@ -45,8 +57,10 @@ export function UpdateQuestionForm({
4557
onSubmit,
4658
action,
4759
onFormStateChange,
48-
databaseList,
60+
fragment,
4961
}: UpdateQuestionFormProps) {
62+
const { databases, questionCategories } = useFragment(QUESTION_UPDATE_FORM_FRAGEMENT, fragment);
63+
5064
const form = useForm<z.infer<typeof formSchema>>({
5165
resolver: zodResolver(formSchema),
5266
defaultValues,
@@ -116,7 +130,7 @@ export function UpdateQuestionForm({
116130
<FormItem>
117131
<FormLabel>分類</FormLabel>
118132
<FormControl>
119-
<Input {...field} placeholder="例如:query, join, aggregation" />
133+
<Input {...field} placeholder="例如:query, join, aggregation" list="question-categories" />
120134
</FormControl>
121135
<FormDescription>題目的分類標籤。</FormDescription>
122136
<FormMessage />
@@ -159,10 +173,9 @@ export function UpdateQuestionForm({
159173
<FormItem>
160174
<FormLabel>參考答案</FormLabel>
161175
<FormControl>
162-
<Textarea
176+
<SQLEditor
163177
{...field}
164178
placeholder="請輸入 SQL 參考答案"
165-
className="min-h-[120px] font-mono"
166179
/>
167180
</FormControl>
168181
<FormDescription>提供標準的 SQL 解答。</FormDescription>
@@ -186,7 +199,7 @@ export function UpdateQuestionForm({
186199
<SelectValue placeholder="選擇資料庫" />
187200
</SelectTrigger>
188201
<SelectContent>
189-
{databaseList.map((database) => (
202+
{databases.map((database) => (
190203
<SelectItem key={database.id} value={database.id}>
191204
{database.slug}
192205
</SelectItem>
@@ -199,6 +212,10 @@ export function UpdateQuestionForm({
199212
</FormItem>
200213
)}
201214
/>
215+
216+
<datalist id="question-categories">
217+
{questionCategories.map((category) => <option key={category} value={category} />)}
218+
</datalist>
202219
</UpdateFormBody>
203220
);
204221
}

app/(admin)/(question-management)/questions/_components/update.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
DialogTrigger,
1212
} from "@/components/ui/dialog";
1313
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
14+
import { graphql } from "@/gql";
1415
import { QuestionDifficulty } from "@/gql/graphql";
1516
import { useDialogCloseConfirmation } from "@/hooks/use-dialog-close-confirmation";
1617
import { skipToken, useMutation, useSuspenseQuery } from "@apollo/client/react";
@@ -19,7 +20,7 @@ import { useRouter } from "next/navigation";
1920
import { Suspense, useState } from "react";
2021
import { toast } from "sonner";
2122
import { QUESTION_UPDATE_MUTATION } from "./mutation";
22-
import { DATABASE_LIST_QUERY, QUESTION_BY_ID_QUERY, QUESTIONS_TABLE_QUERY } from "./query";
23+
import { QUESTION_BY_ID_QUERY, QUESTIONS_TABLE_QUERY } from "./query";
2324
import { UpdateQuestionForm, type UpdateQuestionFormData } from "./update-form";
2425

2526
export function UpdateQuestionDropdownTrigger({ id }: { id: string }) {
@@ -139,6 +140,12 @@ export function UpdateQuestionButtonTrigger({ id }: { id: string }) {
139140
);
140141
}
141142

143+
const UPDATE_QUESTION_DIALOG_CONTENT_QUERY = graphql(`
144+
query UpdateQuestionDialogContent {
145+
...QuestionUpdateForm
146+
}
147+
`);
148+
142149
function UpdateQuestionDialogContent({
143150
id,
144151
open,
@@ -150,10 +157,7 @@ function UpdateQuestionDialogContent({
150157
onCompleted: () => void;
151158
onFormStateChange: (isDirty: boolean) => void;
152159
}) {
153-
const { data: databaseList } = useSuspenseQuery(
154-
DATABASE_LIST_QUERY,
155-
open ? {} : skipToken,
156-
);
160+
const { data: dialogData } = useSuspenseQuery(UPDATE_QUESTION_DIALOG_CONTENT_QUERY, open ? {} : skipToken);
157161
const { data: question } = useSuspenseQuery(
158162
QUESTION_BY_ID_QUERY,
159163
open
@@ -216,7 +220,7 @@ function UpdateQuestionDialogContent({
216220
onSubmit={onSubmit}
217221
action="update"
218222
onFormStateChange={onFormStateChange}
219-
databaseList={databaseList?.databases || []}
223+
fragment={dialogData ?? {}}
220224
/>
221225
</DialogContent>
222226
);

app/(admin)/(user-management)/groups/_components/update-form.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export function UpdateGroupForm({
105105
name="scopeSetSlugs"
106106
value={field.value ?? []}
107107
onChange={field.onChange}
108-
list="scopeSetList"
108+
list="scope-set-list"
109109
/>
110110
</FormControl>
111111
<FormMessage />
@@ -117,7 +117,7 @@ export function UpdateGroupForm({
117117
/>
118118

119119
{/* scope set list */}
120-
<datalist id="scopeSetList">
120+
<datalist id="scope-set-list">
121121
{scopeSetList.map((scopeSet) => <option key={scopeSet.id} value={scopeSet.slug} />)}
122122
</datalist>
123123
</UpdateFormBody>

components/sql-editor.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Button } from "@/components/ui/button";
2+
import { sql, SQLite } from "@codemirror/lang-sql";
3+
import CodeMirror, { type ReactCodeMirrorRef } from "@uiw/react-codemirror";
4+
import { Code } from "lucide-react";
5+
import { useRef } from "react";
6+
import { toast } from "sonner";
7+
8+
export interface SQLEditorProps {
9+
value?: string;
10+
onChange?: (value: string) => void;
11+
placeholder?: string;
12+
}
13+
14+
export default function SQLEditor({ value, onChange, placeholder }: SQLEditorProps) {
15+
const codeMirrorRef = useRef<ReactCodeMirrorRef>(null);
16+
17+
const handleFormat = async () => {
18+
const { formatDialect, sqlite: formatterSqlite } = await import(
19+
"sql-formatter"
20+
);
21+
22+
const formattedCode = formatDialect(value ?? "", {
23+
dialect: formatterSqlite,
24+
keywordCase: "upper",
25+
});
26+
27+
onChange?.(formattedCode);
28+
toast.success("成功格式化 SQL 程式碼");
29+
};
30+
31+
return (
32+
<div className="flex flex-col gap-4">
33+
<CodeMirror
34+
className={"rounded border border-input text-sm"}
35+
ref={codeMirrorRef}
36+
placeholder={placeholder}
37+
value={value}
38+
onChange={onChange}
39+
extensions={[
40+
sql({
41+
dialect: SQLite,
42+
upperCaseKeywords: true,
43+
}),
44+
]}
45+
/>
46+
47+
<Button
48+
variant="outline"
49+
size="sm"
50+
onClick={(e) => {
51+
e.preventDefault();
52+
e.stopPropagation();
53+
handleFormat();
54+
}}
55+
>
56+
<Code />
57+
格式化
58+
</Button>
59+
</div>
60+
);
61+
}

0 commit comments

Comments
 (0)